Skip to main content
Glama
README.md
# aws-team-memory

Shared, centralized memory for a team of Claude agents. Claude Code, Agent SDK agents and claude.ai all connect to one remote **MCP server**, and they read and write the same set of markdown memory files stored in a **versioned S3 bucket**.

It's built for individuals on a Claude Pro or Max plan who want several agents to share memories without Claude's team features. claude.ai's built-in memory keeps working as before. This server runs alongside it as a custom connector; it doesn't replace it.

## How it works

```
Claude Code / Agent SDK ──Bearer token──┐
                                        ├─> API Gateway ─> Lambda (MCP server) ─> S3 (versioned *.md files)
claude.ai custom connector ──OAuth──────┘                       │
                                                  Secrets Manager (agent tokens) · Cognito (claude.ai login)
```

- **Plain files are the source of truth.** Each subject is one markdown file with YAML frontmatter, so memories are easy to read, diff and audit. S3 versioning keeps every overwrite, so any change can be undone.
- **No silent overwrites.** Every read returns a `version` (the S3 ETag). Replacing a file requires that version, and S3 rejects the write if another agent changed the file in the meantime. The agent then gets the newest content back, merges its update into it, and retries against the new version. The retry is conditional too.
- **Every write is attributed.** The server stamps `updated_by` (the agent's name) and `updated_at` into each file.
- **Shared knowledge is curated.** Every agent can read everything, but only *curator* agents can write to `shared/`. Every agent has its own `agents/<name>/` area.

## Storage layout

```
shared/                       curated knowledge (curators only)
  clients/acme.md
  methods/sap-conventions.md
agents/<agent-name>/scratch/  each agent's own working notes
traces/digests/<agent>/<date>/<session>.json       session digests (kept indefinitely; not visible to memory_* tools)
traces/raw/<agent>/<date>/<session>.jsonl.gz       redacted raw transcripts, SSE-KMS (expire after TEAM_MEMORY_TRACE_RAW_DAYS)
```

An example file:

```markdown
---
description: House conventions for statistical analysis plans
updated_by: claude-ai-alice
updated_at: 2026-11-03T21:40:00+00:00
---

# SAP conventions
...
```

Pass the summary as `memory_write`'s `description` argument and the server writes valid YAML for you. If an agent writes the frontmatter itself, an unquoted `: ` inside a flat `key: value` line is accepted and taken literally. Other invalid YAML is rejected with an error explaining how to quote the value.

Agents can't delete files. Experiment under `agents/<you>/scratch/`, and have an operator remove anything written by mistake:

```sh
uv run python -m infra.deploy rm shared/some/file.md [more paths...]   # versioned bucket: old versions stay recoverable
```

## MCP tools

| Tool                                                               | What it does                                                                                                                                                                                                                                                                          |
| ------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `memory_list(prefix="")`                                         | Lists files under a prefix, with descriptions, authors, timestamps and versions.                                                                                                                                                                                                      |
| `memory_read(paths)`                                             | Reads one or more files. Each result includes the `version` you need to edit that file.                                                                                                                                                                                             |
| `memory_write(path, content, if_version=None, description=None)` | Creates a file (omit `if_version`), or replaces one (pass its current `version`). If the file already exists or has changed, nothing is written, and the result is `status: "conflict"` with `current_content` and `current_version` for the agent to merge into and retry. |
| `memory_str_replace(path, old, new, if_version=None)`            | Replaces a single, unique occurrence of `old` in the newest version of a file. If the file changed but `old` is still unique, the edit is applied with `rebased: true`. Otherwise the result is `status: "conflict"` with the newest content.                                 |
| `memory_search(query, prefix="", max_results=50)`                | Searches file contents with a case-insensitive regex or plain text, and returns the matching lines.                                                                                                                                                                                   |

| `trace_summary(since_days=14, agent="", project="")` | Aggregates recent session traces: recurring errors, tool error rates, token use, skills invoked, guidance loaded, and metrics per CLAUDE.md version. `project` is a repo's directory name, such as `my-app`. |
| `trace_get(session_id)` | Returns one session's full digest. Raw transcripts are never served over MCP. |

Paths must end in `.md` and live under `shared/` or `agents/`.

## Prerequisites

- An AWS account, and credentials for a profile that can manage S3, Lambda, API Gateway, IAM, Secrets Manager, KMS and Cognito.
- [uv](https://docs.astral.sh/uv/) and Python 3.11.

## Configuration

Copy `.env.example` to `.env` (which is gitignored) and fill it in. All settings are defined in `src/team_memory/settings.py`, and can also be set as environment variables.

| Variable                                                                                                                                                                       | Default                                     | Purpose                                                                                                                                                                             |
| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `AWS_PROFILE`                                                                                                                                                                | none                                        | AWS CLI profile to deploy with                                                                                                                                                      |
| `AWS_REGION`                                                                                                                                                                 | `us-east-1`                               | Region for every resource                                                                                                                                                           |
| `TEAM_MEMORY_STACK_NAME`                                                                                                                                                     | `team-memory`                             | Prefix for resource names, and the `project` tag                                                                                                                                  |
| `TEAM_MEMORY_BUCKET_NAME`                                                                                                                                                    | *(required)*                              | S3 bucket name, which must be globally unique                                                                                                                                       |
| `TEAM_MEMORY_NONCURRENT_VERSION_DAYS`                                                                                                                                        | `90`                                      | How long old file versions are kept                                                                                                                                                 |
| `TEAM_MEMORY_TRACE_RAW_DAYS`                                                                                                                                                 | `90`                                      | How long redacted raw session transcripts are kept (digests are kept indefinitely)                                                                                                  |
| `TEAM_MEMORY_CURATOR_AGENTS`                                                                                                                                                 | *(empty)*                                 | Comma-separated agents allowed to write `shared/`                                                                                                                                 |
| `TEAM_MEMORY_COGNITO_DOMAIN_PREFIX`                                                                                                                                          | *(empty)*                                 | Login domain prefix for claude.ai OAuth. Leave empty to skip Cognito.                                                                                                               |
| `TEAM_MEMORY_OAUTH_CALLBACK_URLS`                                                                                                                                            | `https://claude.ai/api/mcp/auth_callback` | Allowed OAuth redirect URLs (comma-separated)                                                                                                                                       |
| `TEAM_MEMORY_LWA_LAYER_VERSION`                                                                                                                                              | `30`                                      | AWS Lambda Web Adapter layer version                                                                                                                                                |
| `TEAM_MEMORY_LAMBDA_MEMORY_MB`                                                                                                                                               | `512`                                     | Lambda memory size                                                                                                                                                                  |
| `TEAM_MEMORY_LOG_LEVEL`                                                                                                                                                      | `INFO`                                    | Server log level                                                                                                                                                                    |
| `TEAM_MEMORY_TOKENS_SECRET_ARN`, `TEAM_MEMORY_TRACES_KMS_KEY_ARN`, `TEAM_MEMORY_COGNITO_USER_POOL_ID`, `TEAM_MEMORY_COGNITO_APP_CLIENT_ID`, `TEAM_MEMORY_PUBLIC_URL` | *(written by deploy)*                     | Deploy outputs. Leave these blank.                                                                                                                                                  |
| `TEAM_MEMORY_AGENT_TOKEN`                                                                                                                                                    | *(empty)*                                 | Client side only: the agent token the trace-upload hook sends. Leave it empty to disable uploads. `TEAM_MEMORY_URL` (env only) overrides `TEAM_MEMORY_PUBLIC_URL` for the hook. |

## Deploy

```sh
uv sync
uv run python -m infra.deploy up
```

`up` is idempotent, so run it again after any code or settings change. It does the following:

1. Creates or updates the versioned, encrypted, private S3 bucket.
2. Creates the KMS key for raw session transcripts, requires it for `traces/raw/` with a bucket policy, and re-encrypts any raw transcripts stored before the key existed.
3. Creates the agent-token secret.
4. Creates the Cognito user pool, login domain and app client, if a domain prefix is set.
5. Creates the Lambda's IAM role.
6. Builds the Lambda package and deploys the function.
7. Creates the HTTP API.
8. Writes the outputs back into `.env` and prints the connection details.

To print the connection details again at any time:

```sh
uv run python -m infra.deploy outputs
```

## Add agents and users

Each Claude Code or Agent SDK agent gets its own bearer token. The token is shown once, and only its hash is stored.

```sh
uv run python -m infra.deploy add-agent claude-code     # also rotates an existing token
uv run python -m infra.deploy list-agents
uv run python -m infra.deploy remove-agent claude-code
```

Agent names use lowercase letters, digits, `-` and `_`. To let an agent write `shared/`, add its name to `TEAM_MEMORY_CURATOR_AGENTS` and run `up` again.

claude.ai users sign in through Cognito. Invite yourself first, and Cognito emails a temporary password:

```sh
uv run python -m infra.deploy add-user alice you@example.com
```

That user's agent name becomes `claude-ai-alice`.

## Connect clients

**Claude Code**

```sh
claude mcp add --transport http team-memory https://<api-id>.execute-api.<region>.amazonaws.com/mcp \
  --header "Authorization: Bearer <token>"
```

**Agent SDK** (Python):

```python
options = ClaudeAgentOptions(
    mcp_servers={
        "team-memory": {
            "type": "http",
            "url": "https://<api-id>.execute-api.<region>.amazonaws.com/mcp",
            "headers": {"Authorization": f"Bearer {token}"},
        }
    }
)
```

**claude.ai** (Pro or Max):

1. Go to **Settings → Connectors → Add custom connector**.
2. Enter the MCP endpoint URL.
3. Open **Advanced settings** and paste the **Client ID** and **Client secret** printed by `infra.deploy outputs`.
4. Connect, and sign in with your Cognito user.

## Session traces

To improve `CLAUDE.md`, rules and skills over time, each Claude Code session can upload its trace. The upload has two parts: a **digest** of about 2 KB (prompts, tool calls and errors, tokens, skills invoked, a hash of each guidance file loaded) and the **raw transcript**, with secrets redacted and internal request copies dropped, gzipped to about 15% of its size.

1. Create a token for the machine's agent (`add-agent claude-code`) and put it in `.env` as `TEAM_MEMORY_AGENT_TOKEN`.
2. The project's `SessionEnd` and `PreCompact` hooks in `.claude/settings.json` upload the session automatically. They never block or fail a session. They're committed, so they run for anyone who clones this repo, but they upload nothing unless `TEAM_MEMORY_AGENT_TOKEN` is set.
3. To upload past sessions, or ones that didn't end cleanly, run `python .claude/hooks/trace_upload.py --backfill`. Add `--dry-run --file PATH.jsonl` to preview a digest without uploading.
4. For **other repos**, add the same two hooks to that repo's gitignored `.claude/settings.local.json`, pointing at this checkout by absolute path, for example `"command": "python \"C:/path/to/team-memory/.claude/hooks/trace_upload.py\""` under both `SessionEnd` and `PreCompact`. The repo needs no config of its own, because the uploader falls back to team-memory's `.env`. Backfill with `python <team-memory>/.claude/hooks/trace_upload.py --backfill --project-dir <repo path>`. Digests record `project`, so sessions stay distinguishable.
5. For **Agent SDK** agents, run `python .claude/hooks/trace_upload.py --file <transcript.jsonl>` after a session, or register the same script as a `SessionEnd` hook.

Review the traces:

```sh
uv run python -m infra.deploy traces --days 14              # aggregate across sessions
uv run python -m infra.deploy traces --project my-app     # one repo only
uv run python -m infra.deploy trace SESSION_ID              # one session's digest
uv run python -m infra.deploy trace SESSION_ID --turn 3     # one turn of the raw transcript
```

In Claude Code, `/trace-review` runs these and turns recurring friction into guidance changes. From claude.ai, use the `trace_summary` tool. claude.ai conversations themselves aren't captured.

> The claude.ai OAuth flow against Cognito is the least-tested part of this setup. If sign-in fails, check the Lambda's CloudWatch logs, and confirm that the callback URL in `TEAM_MEMORY_OAUTH_CALLBACK_URLS` matches the one claude.ai uses.

## Run locally

```sh
uv run team-memory          # serves http://localhost:8000/mcp using the AWS resources named in .env
uv run pytest               # unit + end-to-end tests against moto; no AWS needed
```

The local server uses the same Secrets Manager tokens and S3 bucket as the deployed one.

## Security and data handling

- **Every connected agent can read everything.** Keep identifiable client or trial data out of the store. If you must store it, deploy a separate stack (with its own `TEAM_MEMORY_STACK_NAME` and bucket) for each client.
- The bucket blocks all public access and is encrypted (SSE-S3). The Lambda role can only read and write `shared/*`, `agents/*` and `traces/digests/*`, and sign uploads to `traces/raw/*`.
- **Raw transcripts are encrypted with a customer-managed KMS key** (`alias/<stack>-traces`, rotated yearly). The bucket policy rejects any raw upload that doesn't use it. Reading a raw transcript needs `kms:Decrypt` on the key as well as `s3:GetObject`, and every decrypt is logged in CloudTrail. The Lambda can only encrypt (`kms:GenerateDataKey`), never read. Digests and memory files use SSE-S3. `down` keeps the key unless you pass `--delete-bucket`, because raw transcripts can't be read without it.
- **Session traces contain whatever the session saw**: file contents, command output and prompts. The uploader masks agent tokens, AWS keys, JWTs, bearer tokens and `*SECRET*=`/`password:`-style values, but that's pattern matching, not a guarantee. Don't enable trace upload on machines whose sessions handle client or trial data.
- Tokens are stored as SHA-256 hashes. Rotate a token by running `add-agent` again with the same name. Running servers pick up the change within 5 minutes.

## Cost

At personal or small-team scale this costs roughly **$1.50/month**:

- Secrets Manager is $0.40 per secret, and the KMS key is $1/month (S3 Bucket Keys keep request costs negligible).
- Lambda, API Gateway and S3 cost pennies, or fall within the free tier. Traces add about 200–300 KB per session in S3.
- Cognito is free under its monthly active user allowance.

## Tear down

```sh
uv run python -m infra.deploy down                   # keeps the bucket and all memories
uv run python -m infra.deploy down --delete-bucket   # deletes everything, irreversibly
```

## Contributing

`CLAUDE.md` describes the code layout and conventions for Claude Code sessions. The project skills `/update-claude-md` and `/update-readme` (triggered by the rules in `.claude/rules/`) keep both documents current.

## License

[MIT](LICENSE) © 2026 Paul Stevenson

Maintenance

ActivityMaintained
ResponsivenessNo issues