Distributed Memory System
Cross-project memory for Claude Code, enabling local semantic recall and secure, git-versioned markdown storage of reusable knowledge across repositories.
README
Distributed Memory System
Cross-project memory for Claude Code — a learning written while working in one project becomes recall-able in every project, without leaking project-specific facts or secrets.
Claude Code's native memory is keyed per repository: what you learn in project A is invisible in project B. This system layers a shared, git-versioned markdown store plus local semantic recall on top — keeping the good per-project notes you already write, while making the reusable ones available everywhere, and enforcing at the write path that secrets and PII never escape their scope.
It is small (617 lines of Python across 8 files), fully local (no API calls at runtime), and the entire index is a disposable cache you can delete and rebuild from the markdown at any time.
It works — and here's the receipt
This isn't a demo. The store below is live. It was seeded once by migrating a real engineer's accumulated Claude Code memory (24 project stores + a cross-project engineering knowledge base + global instructions → 203 raw candidates → 150 gated, de-duplicated memories), and has since been growing on its own as that engineer works:
| Count | |
|---|---|
| Total memories | 160 |
| — seeded by migration | 150 |
— captured organically since activation (memory_write during real sessions) |
10 |
| Global (cross-project) | 74 |
| Project-scoped | 86 across 32 project scopes |
| By type | project 70 · reference 61 · procedural 21 · feedback 5 · user 3 |
| By domain | ai 64 · infra 29 · db 23 · design 18 · general 16 · bioinfo 10 |
| By sensitivity | internal 83 · public 75 · secret 2 (both project-scoped, redacted at rest) |
The value loop, observed in the wild. In the hours after activation, an engineer rebuilt a Next.js app ("Aurelia") following the built-in sprint workflow. The system captured, unprompted: a deep-research brief, a spec-first test surface, testing gotchas — and five generalizable gotchas it correctly promoted to global scope, e.g.:
drizzle_neon_http_has_no_interactive_transactions_use_neon_serverless— "Drizzleneon-httpthrows ondb.transaction(); useneon-serverless(WebSocket) for atomic writes on Vercel. Local dev =node-postgres; prod =neon-serverless; only the driver import swaps."
That memory was learned in one project. Its body ends with [[nextjs_local_node_postgres_not_neon_http]]
— an automatic link to a memory that was migrated from a different project (TubeIntel).
Two projects, two points in time, one connected knowledge graph. That is the whole thesis in
a single artifact.
There's even a memory the system wrote about itself — use_mcp_memory_db_not_memory_md — a
learning that the MCP store, not scattered MEMORY.md files, is now the source of truth.
Value propositions
- Cross-project recall. A gotcha, convention, or decision written anywhere is retrievable everywhere — by meaning, not just keywords.
- Security enforced on the write path, not the reader's discretion. Every write is scanned
for secrets, PII, and machine-local paths.
globalscope is provably free of secrets and PII (gate-enforced); project scope may hold sensitive facts, but secrets are redacted at rest. A pre-commit hook is the belt to the gate's suspenders. - Local and offline. Embeddings run on-device (fastembed, ONNX/CPU). No data leaves the machine; no per-query API cost.
- Markdown + git is the source of truth. Every memory is a plain, diffable, reviewable file. The search index is a derived SQLite cache — delete it and rebuild from the markdown at any time.
- Scoped, not global-by-default.
globalfor reusable-everywhere knowledge;project:<name>for the rest. Promotion (project → global) is propose → approve, with provenance retained. - Auditable. 617 lines of Python, no framework magic, every decision inspectable.
Architecture
Two planes: a source-of-truth plane (markdown in git) and a derived-index plane (SQLite). The index is always reconstructible from the source; the source never depends on the index.
flowchart TB
subgraph SOT["Source of truth — versioned in git"]
MD["store/**/*.md<br/>one memory = one file<br/>frontmatter + body"]
end
subgraph DERIVED["Derived index — gitignored, rebuildable"]
FTS["memories_fts<br/>SQLite FTS5 · BM25"]
VEC["embedding BLOBs<br/>384-dim float32 · normalized"]
end
subgraph ENGINE["server/ — 617 LOC"]
IDX["indexer.py<br/>content-hash cached"]
SRCH["search.py<br/>hybrid + MMR"]
GATE["gate.py<br/>sensitivity scanner"]
MCP["mcp_server.py<br/>5 tools over stdio"]
end
CC["Claude Code<br/>(any project)"]
MD -->|"parse + embed<br/>(only changed files)"| IDX --> FTS & VEC
CC <-->|"memory_search / memory_write / …"| MCP
MCP -->|recall| SRCH
SRCH -->|reads| FTS & VEC
MCP -->|"write path"| GATE -->|"redacted, scoped"| MD
MCP -.->|"reindex once after write"| IDX
The write path — where security lives
Secrets are contained at the source, not left to the reader:
flowchart LR
W["memory_write<br/>(title, body, scope…)"] --> SCAN["gate.scan()<br/>7 secret · 2 PII · 2 path rules"]
SCAN --> CHK{"scope == global<br/>AND secret/PII?"}
CHK -->|yes| BLOCK["❌ blocked<br/>'use a project scope'"]
CHK -->|no| RED["redact_secrets()<br/>mask value, keep structure"]
RED --> ESC["escalate sensitivity<br/>to match findings"]
ESC --> FILE["write store/…/slug.md<br/>UTF-8, with frontmatter"]
FILE --> RI["reindex once"]
globalscope is gate-enforced clean. A write toglobalcontaining a secret or PII is rejected, not silently downgraded. (Verified across all 74 global memories in this store: zero secret/PII findings.)- Redact at rest, keep the shape. A DSN like
postgresql://user:pass@host/dbis stored aspostgresql://user:«REDACTED:db-password»@host/db— the locator survives, the secret doesn't. - Sensitivity auto-escalates. If you label a memory
publicbut the scanner finds PII, it's promoted tointernalbefore it's written. You can't under-classify by accident.
The read path — hybrid recall
Semantic similarity finds what you meant; keyword search anchors exact terms (library names, error strings, flags). Blended, min-max normalized, then diversified with MMR so the top-k aren't near-duplicates:
score = 0.7 · cosine(query, memory) # vector — semantic
+ 0.3 · bm25(query, memory) # FTS5 — lexical
Real query against the live store — "drizzle neon transaction on vercel":
| memory | score | vec | bm25 |
|---|---|---|---|
drizzle_neon_http_has_no_interactive_transactions… |
1.00 | 1.00 | 1.00 |
pglite_hermetic_drizzle_postgres_integration_tests |
0.67 | 0.76 | 0.46 |
tubeintel_stack_and_architecture |
0.55 | 0.60 | 0.43 |
The exact learning surfaces first, with a related testing memory and the originating project's architecture right behind — the shape you want for "remind me what I know about X."
Design decisions & nuance
The interesting engineering is in the why. Each choice below traded something for something.
1. Markdown + git is truth; SQLite is a cache.
Memories are durable, human-reviewable, and diff cleanly in PRs. The index (FTS5 + embedding BLOBs)
is gitignored and disposable: rm index.db && python server/indexer.py fully rebuilds it. This
means no schema migrations to fear, no lock-in, and a store you can hand-edit or grep. The cost is
a rebuild step — paid for by content-hash caching (below).
2. Content-hash caching — only re-embed what changed.
Each file's SHA-256 is stored alongside its vector. On reindex, an unchanged file reuses its cached
embedding; only new/edited files hit the model. The bulk migration re-embedded 150 files once;
every subsequent memory_write re-embeds exactly one.
3. Sensitivity gate on the write path, not the read path. Filtering secrets at read time trusts every reader forever. Gating at write time means the dangerous data never lands in a shareable scope in the first place — and the guarantee is structural, not behavioral. The gate is deliberately layered: the MCP tool calls it, and a portable git pre-commit hook re-runs it on anything added outside the tool.
4. The gate is heuristic — and was hardened by real data.
The shipped patterns caught password:-style secrets and /home/ paths. Mining a real corpus
exposed two blind spots it would have leaked to global: DSN-embedded passwords
(postgresql://user:pass@…) and Windows absolute paths (C:\…). Both are now rules
(7 secret · 2 PII · 2 path). This is the honest posture: a gate is a strong default, not a DLP
guarantee — so it's designed to be extended, and it was.
5. Scopes + propose→approve promotion.
global knowledge is small, clean, and always-on; project:<name> knowledge is abundant and may
be sensitive. Promotion is never automatic: memory_promote returns a gate verdict and a proposed
move, but a human approves it. Provenance (source_project) is retained so a global memory always
remembers where it was learned.
6. Hybrid retrieval + MMR, with fixed, legible weights. 0.7/0.3 vector/BM25 and MMR λ=0.7 are constants, not a tuned model — chosen because they're explainable and good enough, and because a memory system's failure mode should be "returned something slightly off," never "silently mis-ranked by an opaque scorer." Both signals are min-max normalized per query so neither dominates by scale.
7. Local 384-dim embeddings (bge-small-en-v1.5).
384 dimensions (1536 bytes/vector) is the sweet spot for a personal store of hundreds–thousands of
memories: strong retrieval, tiny footprint, fast on CPU, no API dependency. Vectors are L2-normalized
so cosine similarity is a single dot product.
8. stdio protocol discipline. An MCP stdio server must never write to stdout — it corrupts the JSON-RPC stream. The indexer prints progress, so the server redirects its stdout to stderr around every reindex. Small detail, total protocol failure if missed.
9. UTF-8, always.
The store legitimately contains em-dashes, arrows, and the «REDACTED» guillemets. Every file read
and write pins encoding="utf-8" — because relying on the platform default (cp1252 on Windows)
crashes memory_write the moment a memory contains one of those characters, and corrupts git blobs
even when it doesn't.
The always-on layer
The store holds hundreds of memories, but only a lean index is ever loaded into a session —
full bodies are fetched on demand via memory_search. regen_rules.py generates
~/.claude/distributed-memory.md: the usage protocol plus a one-line entry per global memory,
imported by ~/.claude/CLAUDE.md. This keeps every session cheap while making the whole store
reachable in one tool call.
## Global memories (74) — one-line index; call `memory_search` for full detail
- drizzle_neon_http_has_no_interactive_transactions… [db] — Drizzle neon-http has no interactive
transactions; use neon-serverless (WebSocket) for atomic writes on Vercel
- fastapi_spa_mount_order [infra] — Mount StaticFiles(html=True) LAST, after every API/WS route…
- …
Schema
One markdown file = one memory. Frontmatter + body. Compatible with Claude Code auto-memory
(name / description / metadata.type) and extended for cross-project recall.
---
name: <slug = filename stem>
description: "<one sharp sentence — this is what recall ranks on>"
metadata:
type: user | feedback | project | reference | procedural
scope: global | project:<name>
domain: db | bioinfo | ai | design | infra | general
sensitivity: public | internal | secret
source_project: <origin repo> # provenance, retained through promotion
origin_session: migration | mcp | …
created: <YYYY-MM-DD>
---
<body — "Why:" / "How to apply:" encouraged; [[links]] to related memories>
descriptionis load-bearing: recall ranks on it and the always-on index shows it verbatim.scope: globalMUST be secret/PII-free (gate-enforced) and should avoid machine-absolute paths.sensitivity: secretnever leaves project scope.[[name]]links reference another memory's slug — this is how the knowledge graph forms.
Average body length in the live store: ~506 characters — sharp and single-fact, not essays.
MCP tools
Registered once at user scope, available in every project:
| Tool | Purpose |
|---|---|
memory_search(query, scope?, domain?, project?, k?, max_sensitivity?) |
Hybrid recall by meaning + keywords. |
memory_write(title, body, type, domain, sensitivity, scope|project, links?) |
Gated write; redacts secrets, escalates sensitivity, reindexes. |
memory_promote(name) |
Proposal only — returns the gate verdict for a project→global promotion; a human approves. |
memory_reindex() |
Rebuild the index from the markdown store. |
memory_status() |
Counts by scope/domain + the embedding model. |
MCP stdio servers must never write to stdout — it corrupts the protocol. Keep all logging on stderr.
Component map
| File | LOC | Role |
|---|---|---|
server/embed.py |
21 | Local embeddings (fastembed, ONNX/CPU) |
server/mem.py |
42 | Frontmatter + body parsing, content hashing |
server/precommit.py |
55 | Pre-commit sensitivity gate (belt-and-suspenders) |
server/regen_rules.py |
59 | Generate the always-on protocol + global index |
server/gate.py |
78 | Secret / PII / path scanner + redactor |
server/indexer.py |
80 | Build/refresh index.db, content-hash cached |
server/search.py |
119 | Hybrid BM25 + vector recall, MMR-diversified |
server/mcp_server.py |
163 | MCP server: the 5 tools, over stdio |
| Total | 617 |
How the initial corpus was built (a case study in itself)
The 150 seed memories weren't hand-written — they were mined from an engineer's real, scattered memory artifacts, which is a nice demonstration of the multi-agent workflow the system now recommends:
- Fan-out mining — 6 parallel agents read 40+ source artifacts (24 project
MEMORY.mdstores, a large cross-project knowledge base, global instructions, in-repo CLAUDE.md files) and extracted 203 candidate memories in a structured schema. - Normalization — duplicates merged (e.g.
python -m pipappeared 4×), a fragmented user profile consolidated, and a name collision resolved (two distinct projects both called "NEXUS" → separate scopes). - Security triage — a shared Postgres credential found across ~10 projects was collapsed into a
single
secretmemory (password omitted, since it was being rotated); ~20 machine paths kept in project scope but stripped from every global. - Gated bulk write — every candidate passed through the same
gate.pyused bymemory_write, then a single reindex — with a post-write assertion that no global memory carries a secret or PII.
The full step-by-step process — the six mining agents (with telemetry), the consolidation
decisions, the security triage, and verification — is documented in
docs/MIGRATION.md.
Quick start
git clone <this-repo> && cd claude-memory-system
python -m venv .venv
# Windows: .venv/Scripts/python -m pip install -r requirements.txt
# macOS/Linux: .venv/bin/pip install -r requirements.txt
# Build the index from the markdown store (first run downloads the embedding model, then offline)
.venv/Scripts/python server/indexer.py # or .venv/bin/python
# Try a recall
.venv/Scripts/python server/search.py "postgres transaction on vercel"
# Install the sensitivity pre-commit gate (portable; detects .venv on Windows or POSIX)
cp hooks/pre-commit .git/hooks/pre-commit # or: ln -sf ../../hooks/pre-commit .git/hooks/pre-commit
# Wire the MCP server into Claude Code (user scope = available in every project)
claude mcp add memory -s user -e PYTHONUTF8=1 -- \
/abs/path/.venv/Scripts/python.exe /abs/path/server/mcp_server.py
# Generate the always-on protocol + global index, and import it
.venv/Scripts/python server/regen_rules.py
Limitations & honest trade-offs
Good engineering names its edges:
- The gate is a strong heuristic, not a DLP system. It catches common secret/PII/path shapes and
is easy to extend, but a novel secret format can slip past.
globalscope is the hard boundary; treat project scope as "may contain sensitive facts." - Single-user by design. No auth, no concurrency control beyond git. It's a personal/pair store.
- Retrieval weights are fixed defaults, not learned. Legible and good enough; not SOTA ranking.
- The always-on global index grows with the global count. At 74 lines it's cheap; a store with thousands of global memories would want tiering. Project memories don't have this cost — they're fetched only on demand.
- 384-dim embeddings favor footprint and speed over the last few points of retrieval accuracy a larger model might buy.
Model
markdown + git = source of truth · SQLite = derived, disposable index · local offline embeddings · hybrid vector + BM25 recall, MMR-diversified · sensitivity gate on the write path · scoped, with human-approved promotion · 617 lines, fully auditable.
License
MIT — see LICENSE.
推荐服务器
Baidu Map
百度地图核心API现已全面兼容MCP协议,是国内首家兼容MCP协议的地图服务商。
Playwright MCP Server
一个模型上下文协议服务器,它使大型语言模型能够通过结构化的可访问性快照与网页进行交互,而无需视觉模型或屏幕截图。
Magic Component Platform (MCP)
一个由人工智能驱动的工具,可以从自然语言描述生成现代化的用户界面组件,并与流行的集成开发环境(IDE)集成,从而简化用户界面开发流程。
Audiense Insights MCP Server
通过模型上下文协议启用与 Audiense Insights 账户的交互,从而促进营销洞察和受众数据的提取和分析,包括人口统计信息、行为和影响者互动。
VeyraX
一个单一的 MCP 工具,连接你所有喜爱的工具:Gmail、日历以及其他 40 多个工具。
graphlit-mcp-server
模型上下文协议 (MCP) 服务器实现了 MCP 客户端与 Graphlit 服务之间的集成。 除了网络爬取之外,还可以将任何内容(从 Slack 到 Gmail 再到播客订阅源)导入到 Graphlit 项目中,然后从 MCP 客户端检索相关内容。
Kagi MCP Server
一个 MCP 服务器,集成了 Kagi 搜索功能和 Claude AI,使 Claude 能够在回答需要最新信息的问题时执行实时网络搜索。
e2b-mcp-server
使用 MCP 通过 e2b 运行代码。
Neon MCP Server
用于与 Neon 管理 API 和数据库交互的 MCP 服务器
Exa MCP Server
模型上下文协议(MCP)服务器允许像 Claude 这样的 AI 助手使用 Exa AI 搜索 API 进行网络搜索。这种设置允许 AI 模型以安全和受控的方式获取实时的网络信息。