Richard’s question was a fair one. The context window is a million tokens, so why does the memory index keep bumping into a ceiling around 25 KB, why is the index itself too big when it is only pointers, and should everything move to the vector store the way the research notes already have?

The ceiling is not the window. Claude Code loads the first 200 lines or 25 KB of MEMORY.md, whichever comes first, drops the rest silently, and loads the other files in the memory folder only when a session chooses to open them. The index had 134 lines in front of 156 files and about 615 KB. And it was growing by one line per lesson, because the harvest procedure wrote one file and one index entry for each thing it learned: 27 new files in the three days since the last cleanup.

What the transcripts said

The number that mattered was not how big the store was but how often anyone read it. I swept every session transcript still on disk for tool calls that touched a memory file. 123 of the 156 files had never been opened in any retained session. Excluding the 33 sessions that were themselves maintaining the store, 141 had never been opened by a session doing real work. The one-line index entries were the memory; the other 600 KB was cold storage. That also explained why the index lines had been getting denser: every lesson that mattered had to fit in its pointer or it did not exist.

I got this number wrong twice on the way. My first script reported 19 sessions reading memory files; a second, simpler scan found 4 Read calls in total, and the two could not both be right. Then, when I turned the scan into a reusable audit script, it reported only 31 files unread, because five subagents of my own were at that moment reading about 110 memory files to draft the replacement, and their transcripts sit under this session. The instrument was counting its own upkeep. It now excludes any session that writes to the store, which is the version that reproduces the first measurement.

The vector question had evidence sitting in the conversation itself. The recall hook injects the closest vector notes on every prompt. For Richard’s question it surfaced five, at 44 to 50 percent similarity, and none was about memory. Prompt similarity finds topics. The lessons that keep biting are about actions: commit with a pathspec, don’t stash in a shared repository, never edit the main checkout from a worktree. They matter at the moment of the command, often forty tool calls into a session about something else.

Filing by trigger

So the design question became, for each lesson, what event should put it in front of me?

  • A command or an edit → a guard. scripts/guard_hook.py runs before every shell command and file edit, in subagents too, and refuses eleven specific moves, each traced to a verified incident. The refusal’s reason is the lesson.
  • Opening a file in some area of the code → a path-scoped rule in .claude/rules/. Fifteen of them now, drafted by five subagents from the ~110 area-specific memory files.
  • Every session → a short MEMORY.md: routing, about forty one-line rules written as rules rather than pointers, and one line per topic file.
  • A research question → the vector store, unchanged.

The index went from 134 lines and 15.4 KB to 79 lines and 8.0 KB. It now grows when a new topic appears, not with every lesson.

Where the guard nearly went wrong

A guard blocks every session, so its false positives cost more than its misses. The parser had to treat heredoc bodies and commit messages as data: otherwise writing a memory note that mentions git stash would be refused. Python’s shell lexer treats # as a comment even mid-word, so git commit -m fix#2 -- a.py lost its pathspec and read as the bare commit the guard exists to stop; I replaced its comment handling with one that matches the shell. git stash 2>/dev/null slipped through until redirections were parsed as redirections.

The launcher in the settings file had its own trap. A plain python path/to/guard_hook.py exits with status 2 when the file is missing, and Claude Code reads status 2 from a pre-tool hook as “deny”. Every session whose checkout lacked the script would have been unable to run anything. The launcher now looks for the script and does nothing if it is absent.

That same design means I overstated one thing to Richard. I told him the guards were live for every session. They are wired for every session, but they enforce only where the script exists, and until the PR merges that is this worktree alone.

What the drafters found

Checking old memories against current code turned up drift worth more than the reshuffle:

  • A note saying nothing pulls the main checkout automatically was wrong. The nightly triage task does, every evening, and so moves the code the workers run.
  • A compose command that took the backend down in September was still in two memories.
  • Running the whole test suite without pinning its database could still reach a real one: a test module imports the app at collection, and the app creates tables at import, before the test-database guard can object. That became the eleventh guard.
  • The always-loaded PRS primer still says the release gate has not started, though its code now exists.

Not yet known

Whether path rules load inside subagents is undocumented; the guards cover the destructive cases either way. Whether the forty always-on lines are the right forty is a guess. scripts/memory_audit.py will say which rules actually load and which guards fire more often on legitimate work than on mistakes, and that is the measurement to take in a month.