feat: .shared-memory-project file convention for repo-rooted project ID

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) <noreply@anthropic.com>
This commit is contained in:
2026-05-18 09:17:04 -07:00
co-authored by Claude Opus 4.7
parent 837c43f3d5
commit b6e28b329d
4 changed files with 65 additions and 3 deletions
+1
View File
@@ -0,0 +1 @@
shared-memory
+41
View File
@@ -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 ## HAProxy example
If you run HAProxy at the edge (TLS terminator + reverse proxy), a minimal If you run HAProxy at the edge (TLS terminator + reverse proxy), a minimal
@@ -201,6 +201,26 @@ export default async function ProjectDetailPage({
} }
/> />
<Card className="mb-6">
<CardHeader className="flex items-center justify-between">
<span className="text-sm font-medium">Auto-identify this project</span>
<span className="text-xs text-fg-subtle">.shared-memory-project</span>
</CardHeader>
<CardBody className="space-y-2 text-sm">
<p className="text-fg-muted">
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.
</p>
<pre className="!whitespace-pre-wrap text-xs">{`echo "${project.key}" > .shared-memory-project`}</pre>
<p className="text-xs text-fg-subtle">
Commit it. The directive tool descriptions tell Claude to read this
file at session start before falling back to inference or the
<code className="mx-1">X-Project-Key</code>header.
</p>
</CardBody>
</Card>
{shareRows.length > 0 || isOwner ? ( {shareRows.length > 0 || isOwner ? (
<Card className="mb-6"> <Card className="mb-6">
<CardHeader className="flex items-center justify-between"> <CardHeader className="flex items-center justify-between">
+3 -3
View File
@@ -156,14 +156,14 @@ function withDefaultProject(
const projectIdentify: ToolDef = { const projectIdentify: ToolDef = {
name: "project.identify", name: "project.identify",
description: 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: { inputSchema: {
type: "object", type: "object",
properties: { properties: {
key: { key: {
type: "string", type: "string",
description: 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: { display_name: {
type: "string", type: "string",
@@ -317,7 +317,7 @@ const projectIdentify: ToolDef = {
const memoryWrite: ToolDef = { const memoryWrite: ToolDef = {
name: "memory.write", name: "memory.write",
description: 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: { inputSchema: {
type: "object", type: "object",
properties: { properties: {