Project memory for Claude Code that keeps only what is still true.
Each project gets a SQLite database of atomic facts. The model of the session reads and writes it through two tools, and the answer of every turn is held back until the model has looked the subject up and written what the turn changed. Every fact a search returned must be answered for in the write: kept as is, rewritten, or deleted. Facts are revalidated when they are used, with no background worker and no extra LLM call.
A second mod, injected-context, routes long domain rules into the prompt: a classifier (Jev, by TypeSafe) decides, on every request, which rule files apply.
sequenceDiagram
participant O as Owner
participant M as Session model
participant G as Gate
participant D as Project database
O->>M: request
M->>G: memory_search(terms)
G->>D: text search (+ vector search, optional)
D-->>G: facts
G-->>M: facts + what the owner wrote this turn
M->>G: tries to answer
G-->>M: held: memory not written yet
M->>G: memory_write(ops, kept)
G->>G: was every fact read answered for?
G->>D: one transaction + previous text to write.log
M->>O: answer
- Storage. One
memory.dbper project under~/.claude/projects/<project>/memory/. A fact is one row: topic, kind (state,event,decision,trap), text, date. Search fuses two rankings by rank (RRF): FTS5 bm25, accent-insensitive, and, when configured, bge-m3 vectors in the same file through sqlite-vec. It returns at most 8 facts. A session opened in a subfolder uses the nearest database above it. - Tools.
memory_search(terms)returns the matching facts; a fact already shown in the conversation comes back as its id only.memory_write(ops, kept)applies inserts, updates and deletes in one transaction. - Gate. The final answer of a turn is swapped for a tool call that says what is missing until the turn has both a search and a write. A
Stophook blocks once as a last resort. - Accountability. A write is refused while any fact returned by a search in the turn is missing from
kept,updateordelete. Facts over 600 characters are refused (one subject per fact). - Owner authority. Each search result carries what the owner wrote in the turn, so a fact that contradicts it cannot be kept.
- Audit. Before an update or delete, the previous text goes to
write.logbeside the database. - Current state only. A contradicted fact is rewritten or deleted, never archived with a validity date. The log exists for human audit only.
Facts are written in the language of the owner.
On each prompt typed by the user:
Each request costs two model calls: the translation and Jev's classification.
- Claude Opus translates the prompt to English (Jev is far more precise in English than in Portuguese).
- For each file in
~/.claude/hooks/context/*.mdwith adescriptionin its frontmatter and not yet injected in the session, Jev gets one yes/no question: does the requested work fall under this description? The two previous requests go along as context. Jev sees only the description, never the file name or body. - Files at probability 0.5 or above are injected (body only), once per session and again after a compaction.
- If Jev gives no answer, every pending file is injected, with a toast and a line in
~/.claude/jev.log.
A rule file looks like this:
---
description: Use when the work creates or edits a database migration, a table, a column or an index.
---
## Migrations
- ...- Claude Code with mods (function-hook plugins).
bash,jq,sqlite3with FTS5,curl.- Optional, for search by meaning: the sqlite-vec extension, a
sqlite3that can load extensions (the one shipped with macOS cannot; Homebrew's can) and a server with an OpenAI-style/v1/embeddingsendpoint serving bge-m3 (1024 dimensions), such as llama.cppllama-server --embeddings. - For
injected-context: a TypeSafe API key in~/.config/typesafe/api_key(mode 0600) and rule files in~/.claude/hooks/context/.
Set these in the environment Claude Code runs in (for example under env in ~/.claude/settings.json):
| Variable | Value |
|---|---|
MEMORY_GATE_VEC |
path to the sqlite-vec extension, absolute and without the suffix (e.g. /usr/local/lib/vec0) |
MEMORY_GATE_EMBED_URL |
the embeddings endpoint (e.g. http://localhost:8090/v1/embeddings) |
MEMORY_GATE_SQLITE |
a sqlite3 that can load extensions (default: sqlite3 on the PATH) |
With both MEMORY_GATE_VEC and MEMORY_GATE_EMBED_URL set, each fact gets a vector of its topic and text on write, and each search adds the 30 nearest facts with cosine distance below 0.55 to the 30 best by words. Without them, or when the server does not answer, search uses words only; a fact written while the server is down gets its vector on the next write.
At the prompt of a Claude Code session:
/plugin install memory-gate --marketplace jsilva74/memory-gate
/plugin install injected-context --marketplace jsilva74/memory-gate
Answer y to add the marketplace, then pick a scope. Each mod is self-contained: its scripts and the database schema ship in its own hooks/ folder. The first write in a project creates its database.
Run a mod from this folder with claude --plugin-dir mods/memory-gate (or mods/injected-context). There is no command that generates the TypeScript types: Claude Code writes them to .claude-plugin/types/ inside the mod folder when a session loads the mod. With the types in place:
npx -p typescript tsc -p mods/memory-gate
npx -p typescript tsc -p mods/injected-context
claude plugin test mods/injected-context
claude plugin validate .keptis enforced as an answer, not as proof that the model compared the fact with the turn.- Without search by meaning, search is lexical (FTS5) and a fact worded differently from the search terms may not come up.
- Every turn pays for one search and one write.
- Built for one owner and one agent per project.
MIT