From b6e28b329d5205738675a79039bd6dcafc7b30ba Mon Sep 17 00:00:00 2001 From: jknapp Date: Mon, 18 May 2026 09:17:04 -0700 Subject: [PATCH] feat: .shared-memory-project file convention for repo-rooted project ID MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaces "per-machine X-Project-Key header" as the default way to tell Claude Code which shared-memory project a repo belongs to. Commit a single-line `.shared-memory-project` text file at the repo root; every collaborator's Claude Code reads it at session start and attaches all memories + snippets to the same shared project. No per-machine config required. Changes: - New file convention documented in README (resolution order, format, authoring snippet, rationale for plain-text over JSON). - project.identify tool description now leads with "check .shared-memory-project at the repo root", with inference as fallback. - memory.write description mentions the file as the canonical source for project keys. - Project detail page in the Web UI shows a copy-paste `echo > file` command so users see exactly what to add to their repo. - Added .shared-memory-project to this repo (content: `shared-memory`). Resolution precedence is: explicit tool arg → .shared-memory-project → X-Project-Key header → inference. The file beats the header because repo context is more specific than machine context. This is a soft convention — Claude has to read the file. Directive tool descriptions make this very likely; a Claude Code skill would make it bulletproof, deferred until we see whether the description alone is enough. Co-Authored-By: Claude Opus 4.7 (1M context) --- .shared-memory-project | 1 + README.md | 41 +++++++++++++++++++ apps/web/app/(authed)/projects/[key]/page.tsx | 20 +++++++++ apps/web/lib/mcp/tools.ts | 6 +-- 4 files changed, 65 insertions(+), 3 deletions(-) create mode 100644 .shared-memory-project diff --git a/.shared-memory-project b/.shared-memory-project new file mode 100644 index 0000000..4a678ff --- /dev/null +++ b/.shared-memory-project @@ -0,0 +1 @@ +shared-memory diff --git a/README.md b/README.md index 4794789..76f60aa 100644 --- a/README.md +++ b/README.md @@ -302,6 +302,47 @@ to `/plugin install shared-memory`. Other IdPs that already support DCR --- +## Identifying the current project (`.shared-memory-project`) + +When Claude Code calls memory.write / memory.search / etc., the server needs +to know *which* project the call belongs to. Resolution order, first match +wins: + +1. **Explicit `project` argument** on the tool call. +2. **`.shared-memory-project` file** at the repo root — a single line of + plain text containing the project key. Claude is instructed to read this + first when a project context is present, before inferring or asking. This + is the recommended path for any repo: commit the file, and every + collaborator's Claude Code automatically attaches memories to the same + shared project. +3. **`X-Project-Key` request header** — per-MCP-registration default, set at + `claude mcp add` time with `--header "X-Project-Key: foo"`. Useful when a + machine works in one project across many repos. +4. **Inference** — repo name / git remote slug / working-directory basename, + as a last resort. + +### Adding `.shared-memory-project` to your repo + +```bash +echo "your-project-key" > .shared-memory-project +git add .shared-memory-project +git commit -m "chore: declare shared-memory project key" +``` + +The key must match the regex `^[a-zA-Z0-9._\-/]+$` (same constraint as the +`ProjectKey` Zod schema — alphanumerics plus `.`, `_`, `-`, `/`). Pick +something stable; renaming later is fine but breaks the implicit link with +any pre-existing memories you wrote against the old key. + +### Why a flat-text file, not JSON + +Matches the family of `.python-version`, `.nvmrc`, `.tool-versions` — easy +to grep, easy to author by hand, easy to read from any client without a +parser. If we ever need richer metadata (display name, default tags, etc.) +we'd graduate to a structured format, but the single-key case is the 95%. + +--- + ## HAProxy example If you run HAProxy at the edge (TLS terminator + reverse proxy), a minimal diff --git a/apps/web/app/(authed)/projects/[key]/page.tsx b/apps/web/app/(authed)/projects/[key]/page.tsx index 0405056..7706364 100644 --- a/apps/web/app/(authed)/projects/[key]/page.tsx +++ b/apps/web/app/(authed)/projects/[key]/page.tsx @@ -201,6 +201,26 @@ export default async function ProjectDetailPage({ } /> + + + Auto-identify this project + .shared-memory-project + + +

+ Commit a one-line text file at the repo root so every Claude Code + session opened in this repo automatically targets this project — + no per-machine config needed. +

+
{`echo "${project.key}" > .shared-memory-project`}
+

+ Commit it. The directive tool descriptions tell Claude to read this + file at session start before falling back to inference or the + X-Project-Keyheader. +

+
+
+ {shareRows.length > 0 || isOwner ? ( diff --git a/apps/web/lib/mcp/tools.ts b/apps/web/lib/mcp/tools.ts index 120d790..8da2ee3 100644 --- a/apps/web/lib/mcp/tools.ts +++ b/apps/web/lib/mcp/tools.ts @@ -156,14 +156,14 @@ function withDefaultProject( const projectIdentify: ToolDef = { name: "project.identify", description: - "Call ONCE near the start of every session that has a project context — a repo you're working in, a service you're debugging, etc. — to register or look up that project so subsequent project-scoped memories attach correctly. Use a stable `key` you can reproduce next session (repo name, repo URL, or working directory basename). Returns shared projects you have access to in addition to your own; when an owned and a shared project would both match the same key, the owned one wins (a server-side warning is logged so the collision is debuggable). Skip if the work is purely scratch / not tied to a specific codebase.", + "Call ONCE near the start of every session that has a project context — a repo you're working in, a service you're debugging, etc. — to register or look up that project so subsequent project-scoped memories attach correctly. **Look up the key in this order:** (1) `.shared-memory-project` at the repo root (single-line text file with just the project key; walk up from cwd to find it — same lookup style as `.gitignore` / `.nvmrc`). (2) If no file, fall back to a stable inference: repo name, git remote slug, or working directory basename. The file convention exists so teams sharing a repo all hit the same shared project automatically — prefer it over guessing. Returns shared projects you have access to in addition to your own; when an owned and a shared project would both match the same key, the owned one wins (a server-side warning is logged so the collision is debuggable). Skip if the work is purely scratch / not tied to a specific codebase.", inputSchema: { type: "object", properties: { key: { type: "string", description: - "Stable project identifier. Recommended: repo name, repo URL, or any string the caller can reproduce across sessions.", + "Stable project identifier. First check `.shared-memory-project` at the repo root (one line of plain text, that line IS the key) — that's the canonical source when present. Otherwise use repo name, repo URL, or any string the caller can reproduce across sessions.", }, display_name: { type: "string", @@ -317,7 +317,7 @@ const projectIdentify: ToolDef = { const memoryWrite: ToolDef = { name: "memory.write", description: - "Save a durable fact, preference, or decision that ANY future Claude Code session on ANY of this user's machines should know. Call this when the user shares something that meets ALL of: (1) likely to matter beyond this conversation, (2) not derivable from reading current code/git, (3) would surprise a future you if forgotten. Examples: 'I use HAProxy at home' (user-scope), 'we chose Drizzle over Prisma because of bundle size' (project-scope), 'our prod DB is at db.example.com' (user-scope reference). Use scope='user' for facts about the human or their infra; scope='project' for facts tied to a specific codebase (always preceded by project.identify). In shared projects (i.e. ones surfaced by project.identify with `shared: true`), anyone with rw access can write — your memory becomes visible to every member of every group the project is shared with. Defaults `project` to the X-Project-Key header value if not supplied. Sensitive info (API keys, credentials, connection strings the user actively shares with you) IS appropriate to save here — this server is OIDC-gated and per-user; safer than writing to local container files. DO NOT use for: transient task state, this-session-only scratch notes, or container-specific facts (those belong in the built-in file-based memory at ~/.claude/.../memory/). Tags help retrieval.", + "Save a durable fact, preference, or decision that ANY future Claude Code session on ANY of this user's machines should know. Call this when the user shares something that meets ALL of: (1) likely to matter beyond this conversation, (2) not derivable from reading current code/git, (3) would surprise a future you if forgotten. Examples: 'I use HAProxy at home' (user-scope), 'we chose Drizzle over Prisma because of bundle size' (project-scope), 'our prod DB is at db.example.com' (user-scope reference). Use scope='user' for facts about the human or their infra; scope='project' for facts tied to a specific codebase (always preceded by project.identify; project key should come from `.shared-memory-project` at the repo root when present). In shared projects (i.e. ones surfaced by project.identify with `shared: true`), anyone with rw access can write — your memory becomes visible to every member of every group the project is shared with. Defaults `project` to the X-Project-Key header value if not supplied. Sensitive info (API keys, credentials, connection strings the user actively shares with you) IS appropriate to save here — this server is OIDC-gated and per-user; safer than writing to local container files. DO NOT use for: transient task state, this-session-only scratch notes, or container-specific facts (those belong in the built-in file-based memory at ~/.claude/.../memory/). Tags help retrieval.", inputSchema: { type: "object", properties: {