Every long-running Claude Code project hits the same wall: the conversation ends, the context window empties, and everything the assistant learned about your project — file IDs, past corrections, the "don't ever do that again" rules — disappears with it. Anthropic has an answer for this. So does the plugin ecosystem. For one particular project, neither fit well, so we built a third approach by hand, then ran a knowledge-graph audit against it to see if it actually held together.
This is a write-up of that comparison — three real, working approaches to agent memory, not hypotheticals — using a farming partnership's Google Sheets ledger as the test case. It's a small project on purpose: five acres of sugarcane and banana, three partners, one lender, one spreadsheet. Small enough that if a memory system adds overhead without adding clarity, you notice right away.
The graph isn't the memory. It's the lint pass against the memory.
Section 04 — why this project audits its own docs with a knowledge graph, and what that audit is actually for
That's the one idea worth taking away even if you read nothing else: most projects that reach for a knowledge graph want it to be the memory — a structure smart enough to fetch the right fact at query time. This project's graph isn't that. It's never read at runtime; nothing loads it into context. Its only job is to check, after the fact, whether the plain markdown files a human and an assistant actually rely on still agree with each other. Memory stays boring — themed files, a hook, a routing table. The graph just checks that boring system for drift.
01Three things called "memory"
"Give Claude memory" turns out to mean at least three different engineering problems, each solved by a different part of the ecosystem:
Typed auto-memory
A file-based store the assistant writes to on its own, sorted into four categories: user (who you are, your role), feedback (corrections and confirmations about how you like to work), project (current initiatives, deadlines, decisions), and reference (pointers to external systems, like a Linear board or a Slack channel). Each memory is a small file with frontmatter tags; an index (MEMORY.md) loads every session so relevant entries show up without being asked for.
- Built for who you're talking to and how they like to work — it carries across every project, not just one repo.
- Recall is semantic: the assistant judges what's relevant, which means it can judge wrong.
- Explicitly not for anything you could derive from the code, git history, or a CLAUDE.md — it's for what nothing else already records.
Flat capture, then promote
The pattern behind self-improving-agent-style plugins: everything the assistant learns lands first in one flat, timestamped MEMORY.md — no sorting required up front. A separate review step does the sorting later: a status command reports file size and staleness, a review command flags things worth merging, and a promote command graduates anything durable into CLAUDE.md or a checked-in rules file, where it becomes an enforced convention instead of just a note. Recurring fixes can get pulled out again into standalone skills.
- Built for codebase conventions worth reusing — a debugging trick, a pattern worth enforcing project-wide.
- Cheap to write to (no categorizing up front); the cost is deferred to an explicit review/promote step.
- Assumes a human or a scheduled pass will periodically clean house — unreviewed memory just keeps growing.
An index, a shelf of ledgers, and two hooks
What this project actually runs: a lean index file (docs/PROJECT_NOTES.md) holding only hard rules and a routing table, pointing into themed files under docs/memory/ — one each for partnership structure, Drive file IDs, spreadsheet schema, data corrections, tooling quirks, and open items. A SessionStart hook prints the index into context automatically, every session, no exceptions. A Stop hook catches every attempt to end a turn and asks one question: did anything just happen that belongs in one of these files?
- Built for domain state that must never be silently missed — a file ID, a "never do X" rule — not for assistant preferences.
- Delivery is deterministic (a hook prints a file), not probabilistic (a model guessing what's relevant).
- Plain markdown in the repo: the user can read every word of what the assistant "remembers," and diff it in git.
02Why the shipped answers didn't fit
Typed auto-memory is genuinely the right tool for its job — this project uses it too, elsewhere, for things like "the user prefers no new template versions," a working-style observation. But most of what this project needs to carry forward isn't about how the user likes to work. It's things like:
"Template v2 — fileId
docs/memory/drive-files.md1w-2y_rVRtqXi2SMI9pdDD4FkOTfm6Y-OWd6kIY1Nat4— THIS IS THE ACTIVE FILE. Do not create v3/v4/etc."
That's not a preference to recall when it feels relevant — it's a rule that has to load every single session, full stop, with zero room to be judged "not relevant enough to surface." A file ID either loads into context or it doesn't. A memory system that surfaces it 9 times out of 10 has still failed.
The flat-capture-then-promote pattern has the opposite problem here: it assumes a steady trickle of reusable code patterns worth eventually hard-coding into CLAUDE.md. But there's no code in this project — no build, no lint, no test suite. What's being tracked is external, real-world state that changes: a spreadsheet a human edits by hand between sessions. Promoting facts into rules doesn't have anywhere sensible to put that kind of fact — a bare file ID isn't a "convention," and there's nothing here to extract into a skill.
03What we actually built
The structure is deliberately boring: an index file that stays under fifty lines, and themed files it points into.
Each themed file works like a shelf, not a shoebox: readable on its own, but ending with a Related section that names the neighboring files and — this is the part that matters — why they're connected, not just that they are:
## Related - [`sheet-schema.md`](sheet-schema.md) — the `Partner Ledger` tab in v2 is where these capital/loan balances are actually tracked; check it when reconciling numbers here against the sheet. - [`drive-files.md`](drive-files.md) — v2 is the file that holds the Partner Ledger tab referenced above.
The two hooks are what make the index self-enforcing instead of aspirational. SessionStart fires no matter what — on startup, resume, clear, and compact alike — and just cats the index straight into context:
# Prints docs/PROJECT_NOTES.md into context at session start. NOTES="$CLAUDE_PROJECT_DIR/docs/PROJECT_NOTES.md" if [ -f "$NOTES" ]; then echo "=== docs/PROJECT_NOTES.md (project notes — read before doing anything) ===" cat "$NOTES" fi
Stop is the more interesting half. It fires every time the assistant tries to end a turn and blocks with a reminder to file anything new — but it checks stop_hook_active first, so it only asks once per turn instead of trapping the session in a loop:
input=$(cat)
stop_active=$(echo "$input" | python3 -c "import json,sys; print(json.load(sys.stdin).get('stop_hook_active', False))")
if [ "$stop_active" = "True" ]; then
exit 0
fi
cat <<'EOF'
{"decision": "block", "reason": "Before stopping: if this turn surfaced any new
important information [...], append it to the relevant thematic file under
docs/memory/ [...]. If nothing new came up, just stop again."}
EOF
That last line — if nothing new came up, just stop again — is doing real work. Without an escape hatch like that, a Stop hook that always blocks turns every session into an argument with itself. With it, the hook costs nothing on a quiet turn and catches something real on the turns that matter.
The hooks are only half the mechanism — they're the trigger, not the instruction. Something still has to tell Claude which file a new fact belongs in, when to start a new one instead, and when a file has outgrown itself. That answer isn't worked out fresh each time; it's written down in this project's CLAUDE.md, read at the start of every session alongside the index:
Before doing anything: check the index table and open whichever thematic file(s) are relevant to the current task [...] After doing work: append new info to the relevant thematic file (not the index). If nothing existing fits, create a new file [...] and add a one-line pointer row to the index. Every thematic file ends with a "Related" section [...]. If a thematic file grows past ~100 lines or starts covering more than one sub-topic, split it further, update the index, and carry its Related links to the new files.
Call this the third artifact, alongside the two hook scripts: the hooks force the question to get asked; this is the answer key for what to do once it has. It's also close to a literal migration prompt — this is roughly what got pasted in to turn asksealed.com's 217-line flat CLAUDE.md into seven themed files in the first place, and what a from-scratch project would start from rather than reinvent.
One entry in data-corrections.md is the clearest case for why "just fix the number" isn't enough:
V008 (Bank Transfer to owner): ₹2,75,000 → ₹2,50,000, corrected 2026-07-23, confirmed against receipt.
If the file only showed the current value, a future session re-reading old context — a cached tool result, an export, a partner's question — could bring the wrong figure back as fact, with nothing around to contradict it. The changelog line means a correction can't silently erase what was believed before — the same reason double-entry bookkeeping keeps a record of the reversal, not just the corrected balance.
04Auditing the shelf
A set of files with hand-written "Related" links has an obvious failure mode: the links are only as complete as whoever wrote them remembered to make. So this project runs a knowledge-graph pass over docs/ now and then — not as a permanent memory store, but as a lint pass against the memory that already exists.
It's worth being clear about what kind of mechanism this is, because it's different from the two hooks above: graphify runs on purpose — /graphify docs/, typed in when someone decides it's time — not triggered automatically by anything. The hooks are unconditional, firing every session or every turn with no judgment involved. The audit is the opposite: a deliberate, occasional decision. It isn't on a schedule either — run it after a new theme gets added or a file's grown noticeably, not on a timer, and specifically not the same day a structure is first built. The forea-tools case below is the concrete argument for that last rule.
The first run over the current file set found:
| Metric | Result |
|---|---|
| Nodes / edges | 47 nodes, 63 edges across 7 files (~1,481 words) |
| Extraction confidence | 87% directly extracted, 13% inferred, 0% ambiguous |
| Communities detected | 12 — from a 9-node "v2 Editing Rules & Apps Script" cluster down to five single-node islands |
| God nodes | Template v2 (Active File) (10 edges) and v2 Spreadsheet Schema (8 edges) — the two things nearly everything else refers back to |
| Genuine gap found | 1 — open-items.md's "stick with the spreadsheet" call and PROJECT_NOTES.md's "no new template versions" rule were the same underlying judgment, undocumented as such |
That last row is the payoff. Both facts were true on their own, written on different days in different files — the same underlying instruction, given twice, without either of us noticing it was the same instruction. The graph's "surprising connections" list flagged them as semantically similar at 0.8 confidence, a link neither file's author had made, because neither draft had reason to look sideways at the other. The fix was one added sentence in open-items.md, pointing back at the hard rule and naming the shared reasoning out loud.
A simplified view of the same graph — the index as hub, six themed files as spokes, with the one inferred cross-link the audit surfaced drawn in dashed brick-red:
The report is honest about its own limits too: it flagged 23 nodes as weakly connected — mostly single facts, like a dropdown's valid values or a hard rule with no natural neighbor. Those don't need a community; they just need to be findable from the index. Not every fact benefits from being woven into the web — some are fine standing alone.
This isn't a one-off. Three unrelated projects on the same machine landed on the identical index-plus-hooks structure on their own, each for a different reason:
skillcapita.com(a marketing site, nothing like farm accounting) ran a graphify audit that flagged its single gitignored notes file as a thin, two-node cluster: "too small to be a meaningful cluster — may be noise." Four days later, unprompted, its owner replaced that file with the same index-plus-hooks shape — and it caught a real gotcha within a day: Claude Code silently appends permission grants to a settings file mid-session, breaking an edit unless it's re-read first.asksealed.com, a shipped SaaS product with a live database and real users, had let itsCLAUDE.mdgrow to 217 flat, unstructured lines — harder to skim every time something new got learned. Migrating it produced the identical shape a third time (itsStophook uses plaingrepinstead of Python — same contract, different bet about what's installed). The migration also caught something a graph audit can't: the new files had been drafted from a summary of the old one, not a fresh read, and re-reading it turned up a changed pricing figure, a secret wrongly marked unset, and one whole security incident missing outright.forea-tools, a live platform with its own test suite, got the same shape a fourth time — and was deliberately audited the same day it was built, specifically to test whether "wait before auditing" was a real rule or just caution. It was both: the audit found 3 genuine missed cross-links, real value, but also 55 of 101 nodes that were just noise — confirming, the hard way, that audits earn more the longer a structure has had time to accumulate real drift.
05Where each approach actually wins
| A · Implicit Memory | B · Accumulated Memory | C · Deterministic Memory (ours) | |
|---|---|---|---|
| Unit of memory | One typed fact per file (user / feedback / project / reference) | One flat timestamped entry, sorted later | One themed markdown file with a Related section |
| Delivery | Semantic recall — assistant judges relevance | Loaded on demand / at review time | Deterministic — hook prints it every session |
| Best for | Durable facts about the user, portable across projects | Reusable code patterns worth eventually enforcing | Domain state a rule must never silently miss |
| Failure mode | Right fact, wrong session — recall misses it | Grows unreviewed if no one runs the promote step | Manual — cross-links rot unless audited |
| Auditability | Frontmatter-tagged files, not diffed by the user day to day | One flat file, easy to skim, promotion is a judgment call | Plain markdown in the repo, git-diffable, human-reviewed |
| Chosen when | — | — | Small corpus, external mutable state, near-zero tolerance for a missed rule |
06Takeaways
Match the mechanism to the failure cost
Semantic recall is fine when missing a memory once is a minor annoyance. It's the wrong tool when being missed even once — say, overwriting the wrong spreadsheet file — is the whole risk.
An index is not a memory dump
The index file works because it stays under fifty lines and holds only hard rules and a routing table — the moment detail creeps in, it stops being scannable and starts being just another file to search.
Audit the links, not just the facts
The graph pass didn't find a wrong fact anywhere — every file was individually correct. What it found was two true things that didn't know they were the same thing. That's exactly the kind of error hand-written cross-references will always be prone to.
07A fourth candidate arrives
Days after all this ran, Google Cloud published the Open Knowledge Format (OKF v0.1) — a real spec, not another RAG rebrand: one markdown file per concept, a required type: frontmatter field, cross-linked by ordinary markdown links, with reserved filenames like index.md. It's close enough in spirit to everything above — plain text, git-tracked, readable by humans and agents, no vector database in sight — that it's worth asking directly: should this project's shelf be speaking OKF instead of six themed files with hand-written Related sections?
OKF — one file per concept
Where this project's themed files bundle a topic's related facts into prose — partnership.md holds three partners and a lender in one file — OKF splits things apart: one file per partner, mandatory frontmatter, links doing the work prose does here. Its target problem is real, just not this project's problem: an organization where a metadata catalog, a wiki, and agents built by different vendors all need to read the same knowledge without agreeing on anything beyond a file format.
- Solves cross-tool, cross-team interoperability — many producers, many consumers, no shared vendor.
- Takes no position on delivery — it's a file format, not a loader. It wouldn't replace a
SessionStarthook; it would just be what the hook prints. - Finer-grained than this project has ever needed: one concept per file, versus six themed files for an entire ledger.
The honest answer is no, for the same reason Section 02 gave for the other two shipped answers: match the mechanism to the actual problem. OKF's whole value is portability across producers and consumers who don't share a vendor. This project has exactly one consumer — this Claude Code session, gated end to end by two hooks it already controls. Splitting partnership.md into three separate concept files with mandatory type: partner frontmatter wouldn't make anything easier to find here — it would just be extra process with nothing to justify it.
What OKF does confirm — arriving independently, from Google rather than from inside this project — is the shape underneath all four approaches now on the table: plain markdown, git-tracked, cross-linked, readable by whoever opens it next, human or agent, beats anything that needs a vendor SDK first. Where it differs from what's built here is granularity and audience, not philosophy.
08A fifth, wearing a spec sheet
The other kind of candidate that keeps circulating doesn't come from a vendor at all — it's a template: a monospace "reference architecture" one-pager, versioned like a release (v1.0), diagramming a five-zone personal knowledge base. raw/ holds immutable source material — articles, PDFs, voice transcripts. wiki/ holds structured, cross-linked notes distilled from it. output/ holds generated deliverables. ctx/ holds session and prompt scaffolding. mem/ holds identity — CLAUDE.md, preferences, goals. The whole thing loops: retrieve, use, output, then feed new insight back into the wiki and, notably, back into CLAUDE.md itself.
raw / wiki / output / ctx / mem
It shares real principles with what's built here — raw as immutable ground truth is a more general version of this project's "never overwrite the live spreadsheet" rule; a linked wiki is this project's themed files with Related sections, just in different words. But the diagram is a data model, not a delivery mechanism, and three gaps matter more than the resemblance:
- "Retrieve automatically" is undefined. No hook, no trigger — an embedding index is listed as merely optional. That's the same "right fact, wrong session" risk Section 02 raises against typed auto-memory, with nothing as deterministic as a
SessionStarthook to close it. - No size discipline. "Compounding value" is treated as pure upside; "prune, dedupe" is one bullet in a five-step automation list, with no schedule and nothing like this project's "index under 50 lines, split a file past 100" rule.
CLAUDE.mdsits inside the auto-update loop. The diagram feeds new insight back into identity and instructions as part of the same automated cycle that touches raw sources — a much bigger risk than an index that only changes through an explicit, human-readable edit.
Neither got adopted, for a familiar reason: it's built for a whole professional life spanning many domains — voice notes, presentations, a personal wiki — not one small accounting project. The five zones would be real infrastructure with nowhere to put anything; this project has a spreadsheet, a handful of Drive file IDs, and a correction history, not a research archive. What's worth keeping is the vocabulary — "raw as ground truth," "everything linked" — a reminder that this project already committed to both, just without a diagram claiming to be v1.0 of anything.
09Where this breaks down
Everything above argues for this shape. Here's where it strains, stated as plainly as the advantages were — an architecture is only as credible as the failure modes it admits to.
Filing is still a judgment call
The Stop hook guarantees the question gets asked every single turn — that part is deterministic. It doesn't guarantee the answer is complete, filed in the right themed file, or worded so a future session can find it. That's still the assistant's judgment, same as every other approach here. Determinism buys reliable delivery of the reminder, not reliable content in what gets written down because of it.
The audit only catches drift after the fact
graphify found one real gap in this project's own docs and three in forea-tools's — genuine value, shown above. But it's a lint pass, not a linter that runs on save: a Related link can go stale the day after an audit and sit wrong until the next one, and by design that's not every session. The 55-of-101 noisy-node ratio in forea-tools's same-day run is the flip side of the same coin — audit too early, and most of what it flags isn't signal yet either.
It scales by addition, not by structure
Six themed files with one routing table works because there are six. Nothing in this architecture says what happens at twenty — at some point the index itself needs a second level, and no rule here says when that split happens or what it looks like. The only sizing discipline that exists (split a file past ~100 lines) governs individual files, not the shelf as a whole.
Nothing detects a contradiction
graphify's "surprising connections" catches two files stating the same thing without knowing it — that's how the spreadsheet/no-new-versions link got found. It has no mechanism for catching two files stating opposite things. That would surface as confusion mid-session, not as a report finding, and nothing here would flag it in advance.
The hooks are a real dependency, not a free lunch
Bash plus Python (or a portable grep) has to exist in whatever environment runs this. That's not hypothetical — asksealed.com's Stop hook swapped the python3 -c parse for raw grep specifically because it couldn't assume Python would be there. The shape travels; the exact implementation doesn't, and every adopter has to work out that dependency question for their own environment.
None of this means semantic recall is broken. It means different kinds of memory need different delivery mechanisms, and most real systems will want more than one. A fact that must never be missed — a file ID, a hard rule — wants a deterministic hook. A preference that's genuinely fuzzy, like how someone likes feedback phrased or what tone lands, is exactly what recall-based memory is for; forcing it into a themed markdown file wouldn't make it more reliable, just more rigid. This project needed almost none of the second kind, which is why it leans so hard on the first. A bigger project would want both, on purpose, not as a compromise.
None of this generalizes to "always hand-roll your memory system." For a solo developer's day-to-day working style, typed auto-memory does exactly what it should — this project uses it too, for the parts that are actually about how its user prefers to work. For a codebase building up debugging lore worth enforcing, the promote-to-rules pattern earns its keep. What actually pushed this project toward two small shell scripts and a folder of markdown was narrower than "memory is hard": a handful of facts that had to load every single time, in a project too small to need anything more automatic than a cat.