Distributed Memory System
by jrapisarda
README.md
# Distributed Memory System
**Cross-project memory for [Claude Code](https://claude.com/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`** — "Drizzle
> `neon-http` throws on `db.transaction()`; use `neon-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. `global` scope 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](https://github.com/qdrant/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.** `global` for 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.
```mermaid
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:
```mermaid
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"]
```
- **`global` scope is gate-enforced clean.** A write to `global` containing 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/db` is stored as
`postgresql://user:«REDACTED:db-password»@host/db` — the locator survives, the secret doesn't.
- **Sensitivity auto-escalates.** If you label a memory `public` but the scanner finds PII, it's
promoted to `internal` before 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.
```markdown
---
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>
```
- `description` is **load-bearing**: recall ranks on it and the always-on index shows it verbatim.
- `scope: global` MUST be secret/PII-free (gate-enforced) and should avoid machine-absolute paths.
- `sensitivity: secret` never 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:
1. **Fan-out mining** — 6 parallel agents read 40+ source artifacts (24 project `MEMORY.md` stores,
a large cross-project knowledge base, global instructions, in-repo CLAUDE.md files) and extracted
**203 candidate memories** in a structured schema.
2. **Normalization** — duplicates merged (e.g. `python -m pip` appeared 4×), a fragmented user
profile consolidated, and a name collision resolved (two distinct projects both called "NEXUS"
→ separate scopes).
3. **Security triage** — a shared Postgres credential found across ~10 projects was collapsed into a
single `secret` memory (password omitted, since it was being rotated); ~20 machine paths kept in
project scope but stripped from every global.
4. **Gated bulk write** — every candidate passed through the same `gate.py` used by `memory_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`](docs/MIGRATION.md).
---
## Quick start
```bash
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. `global` scope 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](LICENSE).
This server cannot be deployed
Maintenance
ActivitySlowing
ResponsivenessNo issues