English · Русский
File-based persistent memory for Claude Code (or any CLI coding agent). No RAG, no vector database, no SaaS — markdown files, a git repo, and a set of conventions that keep an agent's memory useful for months instead of letting it collapse into a junk drawer.
This is a sanitized template of a system I run daily. Numbers from the live instance after ~3 months: 200+ memory files, ~650 wiki-links between them, an index that loads into every session at under 17KB, two machines, parallel agent sessions most days, and more than one model reading the same vault.
The template ships with example content for a fictional developer (Remy) and fictional projects (acme-crm, pixelgarden). Replace with your own.
Every session starts from zero. The context window ends; the project doesn't.
The usual fixes don't hold up:
- A fat CLAUDE.md turns into a scroll of everything you ever thought. The agent reads 40KB of stale instructions to find the three lines that matter today.
- Dumping session summaries into a folder is write-only memory. Nothing links, nothing dies, nobody reads it — least of all the agent.
- RAG over your notes adds infrastructure, hides why a fact was retrieved, and the agent can't cleanly edit what it half-remembers.
What actually works, in my experience: treat memory like a codebase, not a log. Small files with owners and lifecycles, a tiny index that's always loaded, hard size budgets, and rot control as a first-class rule. The agent maintains it with the same tools it already has — read, write, grep, git.
┌─────────────────────────────────────────────────────────────┐
│ BEHAVIOR CLAUDE.md (stable, small) │
│ how to work: style, decision rules, memory discipline │
├─────────────────────────────────────────────────────────────┤
│ INDEX memory/MEMORY.md (<17KB, always on) │
│ one line per fact-file: [[name]] + relevance hook │
├─────────────────────────────────────────────────────────────┤
│ FACTS memory/*.md (atomic, on demand) │
│ user_* · feedback_* · project_* · reference_* │
│ read only when the index hook says it's relevant │
├─────────────────────────────────────────────────────────────┤
│ BOARDS AGENDA · ORCHESTRATION · COMMS (operational)│
│ what burns · what's next · who holds what — rotated hard │
├─────────────────────────────────────────────────────────────┤
│ PRIVATE journal_* · credentials* (gitignored) │
│ exists locally, never leaves the machine │
└─────────────────────────────────────────────────────────────┘
git = transport between machines
Obsidian = optional lens on top
Five layers, five different rates of change. The behavior layer changes monthly, the index weekly, boards daily, facts whenever something worth keeping happens. Mixing them in one file is how memory systems die.
Every fact-file has a prefix that is its type, and a frontmatter block the tooling can rely on:
---
name: project-acme-crm
description: "one-line relevance hook; quoted if it contains a colon"
type: project | feedback | reference | user
status: active | planned | broken | resolved | idea # when a lifecycle applies
area: user | meta | app | infra | tools | misc
context: work | personal | shared
---user_*— who the user is: role, stack, schedule, preferences. Slow-changing.feedback_*— how to work with them: corrections and confirmed approaches. Each one carries**Why:**and**How to apply:**lines, because a rule without its reason gets misapplied within a month.project_*— living state of a project: decisions with reasons, current phase, blockers, deploy quirks. Anything not derivable from the code or git history.reference_*— stable lookups: gotchas, commands, port tables, "this API lies about X".
One fact per file. Files link to each other with [[wiki-links]] — link generously; a link to a file that doesn't exist yet marks something worth writing, not an error.
This is the example vault that ships with the template — solid arrows are real links, dashed ones point at files not written yet:
graph LR
classDef project fill:#1a7f37,color:#fff,stroke:none
classDef user fill:#8250df,color:#fff,stroke:none
classDef feedback fill:#9a6700,color:#fff,stroke:none
classDef ref fill:#57606a,color:#fff,stroke:none
classDef broken fill:#cf222e,color:#fff,stroke:none
classDef arch fill:none,color:#8b949e,stroke:#8b949e,stroke-dasharray:3
classDef ghost fill:none,color:#8b949e,stroke:#8b949e,stroke-dasharray:6
profile[user_profile]:::user --> ship[feedback_ship_small]:::feedback
glossary[user_glossary]:::user --> ship
glossary --> pg
answer[feedback_answer_first]:::feedback --> profile
schema[feedback_ask_before_schema_changes]:::feedback --> profile
schema --> auth
crm[project_acme_crm]:::project --> k6[reference_k6_loadtest_recipe]:::ref
crm --> ship
k6 --> vps1[reference_vps1_deploy_gotcha]:::ref
shipmate[reference_shipmate_api_quirks 🔴]:::broken --> crm
auth[project_crm_auth_rework ✅]:::project --> incident[archive/incident_2026-06]:::arch
pg[project_pixelgarden]:::project --> trading[project_pixelgarden_trading 🟡]:::project
pg --> spectator[project_pixelgarden_spectator 💡]:::project
pg --> vps1
crm -.-> pdf([reference_invoice_pdf_pipeline]):::ghost
pg -.-> ws([reference_ws_scaling_notes]):::ghost
schema -.-> backup([reference_postgres_backup]):::ghost
Status emojis in the index stay in sync with status: in the file: 🟢 active · 🟡 planned · 🔴 broken · ✅ resolved · 💡 idea. Files under archive/ get status: archived. scripts/normalize_frontmatter.py fixes drift idempotently (dry-run by default, --apply to write); the file body is never touched.
MEMORY.md is the only fact-layer file loaded into every session. One line per memory:
- [[project_acme_crm]] 🟢 — invoice module on staging; load-test before prod; Celery-vs-tasks fork open
The line is a relevance hook, not a summary — its only job is to let the agent decide whether to open the file. The moment you start summarizing content into the index, it bloats, the budget breaks, and recall quality drops because everything looks equally important.
Budget: <17KB, hard. When it's tight, hooks get shorter or files get merged — the budget is an invariant, not a wish.
Three files that would be one messy TODO list in a naive system:
AGENDA.md— what burns + deadlines. 1–2 lines per item plus a[[link]]to the detail. Done items move toarchive/within ~2 days.ORCHESTRATION.md— the forward hub: an 📥 inbox (any task mentioned in passing gets captured here immediately, before it's lost), a portfolio table (project · stage · blocker · next step), a "where the user is the bottleneck" table, and a journal of prompt patterns that failed and how they were fixed.COMMS.md— a message board for parallel agent sessions. When two or three sessions work the same machine at once, they coordinate here instead of routing everything through the human: append-only feed, each session declares territory (files, dirs, branches it holds) before starting, others acknowledge with a✅ ackline. It's async, not realtime — sessions read it at start and before touching shared repos. One rule written in blood: parallel work in a shared repo goes throughgit worktree, never two sessions checking out branches in one working copy.
Boards carry pointers, not content. Detail lives in exactly one place — the project file — and boards say "see [[there]]". Every violation of this rule I've committed came back as a sync bug within two weeks.
Big project files split into two zones:
## NOW (2026-07-25)
Current state, next step, open forks. REWRITTEN in place every session.
## Chronicle
Append-only dated entries below. Never edited, only added to.The agent resumes a project by reading ## NOW, not by re-deriving state from a 90KB chronicle. And a rule that sounds obvious until it saves you: stale state is worse than no state. If the agent isn't sure ## NOW is fresh, it says so instead of confidently acting on last month's world.
Memory systems don't fail by losing data. They fail by keeping all of it.
- Budgets are invariants: index <17KB, boards ~10KB each, COMMS tail ~7 days. Mine drifted to 3–5× budget within three weeks the one time I treated budgets as advisory.
- Rotation is scheduled, not aspirational: a daily cron auto-fixes frontmatter drift; a weekly cron splits mixed blocks (open tails stay, closed noise goes to
archive/) and reports what it did. - Archive, don't delete:
archive/keeps history greppable without loading it into anyone's context. - Deprecate wrong facts on sight — a memory that's wrong is worse than no memory, because it arrives wearing the authority of your own past self.
This repo eats its own cooking: CI runs scripts/validate_vault.py on every push — index budget, index links resolving to real files, frontmatter canon, emoji/status: sync. If the example vault drifts, the badge goes red.
Some files exist locally and never leave the machine: journals, credentials, anything personal. The pattern is enforced by .gitignore, not discipline:
private_*.md
journal_*.md
credentials*.mdThe agent's rule mirrors it: private files are never indexed in MEMORY.md, never quoted in work context. If other models get access to the vault, private layers are read-only-off-limits for them.
- Machines: the memory folder is a git repo; a private remote is the transport. Pull at session start, push after every working block — the other machine only sees what's pushed.
- Sessions: COMMS + territory declarations + worktrees. Covered above.
- Models: nothing here is Claude-specific. In my live setup a second CLI agent running a different model reads and writes the same vault; non-default authors tag their edits (
<!-- edited-by: ... -->) so provenance survives.
The memory folder doubles as an Obsidian vault: the graph view shows clusters per project, colors by type, broken-status files glow red on top. Frontmatter is kept top-level (not nested) specifically so Obsidian Bases can build dashboards over it. .obsidian/ stays gitignored except app.json and graph.json, which are worth syncing.
None of this is required — the system is plain markdown and works headless. But a graph of 200 linked memories is also just genuinely useful for spotting orphans and dead clusters.
- Hit Use this template on GitHub (or just copy
memory/), then move the vault somewhere your agent can reach and make it a private repo — the template is public, your memory shouldn't be. - Take
CLAUDE.md.template, adapt it, and install it as your globalCLAUDE.md. The only hard integration the system needs is the session-start rule it contains: readMEMORY.md, thenAGENDA.md, thenORCHESTRATION.md, thenCOMMS.md. - Replace the example content (Remy, acme-crm, pixelgarden) with your own
user_*/project_*files; update the index. - Adopt the habits below. The files are 20% of the system; the habits are the rest.
- Save immediately, not "later". A fact captured at the end of the session is a fact captured never.
- Update
## NOW+ the boards as part of closing any working block — same reflex as committing code. - Capture stray tasks into the inbox the moment they're mentioned, even mid-task on something else.
- One fact, one file, one place. Boards point, never copy.
- Respect the budgets weekly, ideally by cron.
- Verify before acting on memory: if a memory names a file, flag, or endpoint, grep that it still exists before relying on it. Memories are point-in-time observations, not live state.
- Boards decay into chronicles if closed items with one open tail are allowed to linger — the fix is a split method: pull the open tail out, archive the block.
- Detail duplicated across 4–5 files will desync; you'll discover it when the agent acts on the stale copy.
- An agent almost deleted a live service of mine acting on a stale association from memory. Since then: before destructive ops, read the project file in full, and treat memory claims about infra as hypotheses to verify.
- Index lines drift from hooks into summaries under pressure; a weekly pass rewriting them short keeps recall sharp.
- Emoji status in the index and
status:in files desync without a normalizer. Automate the boring invariants.
MIT