[ 01 / 11 ]The wall worked on its builder first
Twice today, my own pre-commit hook rejected my own commit: [git] REJECTED: MCP recall stale. More than 30 minutes had passed between reading the shared memory and trying to land work on top of it. The gate does not know or care that I built it.
I am claiming that as a success, so I should explain the failure it replaced. The setup: one operator, one codebase - the security-audit harness from the Auditooor post, the tooling behind the audit competitions I compete in - and multiple agents working it in parallel lanes. Each agent is individually competent. Collectively they were amnesiac: every session started cold, every subagent colder, anything learned trapped in one context window until it evaporated. A multi-agent system without shared memory is not a team; it is the same intern hired fresh every morning.
[ 02 / 11 ]The problem: relearn and clobber
Relearning: models drift back into re-deriving settled conclusions within a session. Across agents it is worse: Codex proves a path futile, and an hour later Claude cheerfully re-hunts it. One of those rediscoveries costs about 20 minutes - my estimate from watching logs, not a measurement - and it recurs daily.
Clobbering: two agents in one working tree eventually destroy each other's work. The common version: a sweeping git add absorbs a sibling's half-finished files into the wrong commit. The brutal version, learned the hard way: an integration lane's hard reset destroyed a sibling lane's uncommitted edits. Agents stomping each other's files is its own form of memory loss - history that never got the chance to exist.
I already had the substrate everyone recommends: an Obsidian vault of structured notes, append-only JSONL ledgers of what had been tried, agents instructed to read them. Advisory memory fails silently; nothing about a notes folder makes reading it load-bearing.
[ 03 / 11 ]The principle: fail-closed at three boundaries
What worked was changing memory from a suggestion into a precondition. The principle, as written in my own notes: you cannot write history until you have read it, and a fresh subagent cannot start cold.
[ You cannot write history until you have read it. ]
Fail-closed is the operative property: the checks default to rejection, and they sit at process boundaries, not inside any model's instructions - at the three places where work becomes permanent:
Git. No commit, merge or push without proving you pulled shared context first - and citing the hash of exactly what you pulled.
File writes. No staging outside your declared lane; no destructive git operation that would wipe a sibling's in-flight work.
Subagent spawn. No dispatch unless shared context is already injected into the worker's first prompt.
Behind those three rows: 12 Claude Code hook scripts, 9 git hook scripts, 5 PATH shims.
[ 04 / 11 ]The substrate: 122 reads, one write
The vault is deliberately boring: an Obsidian folder of markdown notes, JSONL ledgers next to them. Greppable, nothing clever.
The interesting part is the API in front of it: one MCP server, vault-mcp-server.py, 36,317 lines, exposing 122 vault_ callables. Exactly one mutates state: vault_remember, which requires an HMAC-signed token with remember scope, persists one markdown note, and updates the index. The other 121 are reads. The ratio is deliberate: agents consume history constantly and append rarely.
The reads that matter daily: vault_resume_context returns a resume pack - where work stopped, what is in flight. vault_known_dead_ends answers a candidate query with known futile workflows, at most 10 JSONL rows. vault_issue_session_token mints workspace-bound HMAC tokens, default TTL 14400 seconds.
A context pack is a schema-versioned, hash-addressed JSON file on disk - auditooor.vault_context_pack.v1:resume:<16-hex>.json - 27,500 to 38,604 bytes across the stored packs. Each carries its own context_pack_id and context_pack_hash; those two fields are what the machinery keys on.
[ 05 / 11 ]Boundary one: git
The git boundary has two layers, because agents reach git through tool calls a framework can intercept and through raw shell where nothing is watching.
Layer one is a PATH shim. My shell profile prepends ~/.auditooor/bin, which shims git, gh, codex, forge and kimi. The git wrapper gates exactly 7 subcommands - commit, push, am, merge, rebase, cherry-pick, revert - and passes read-only ones (log, status, diff, show, fetch) straight through. Any process that types git commit hits the wall, whatever is driving the keyboard.
Layer two is the repo's own hooks, wired via core.hooksPath. pre-commit refuses if the recall sentinel is older than 1800 seconds - 30 minutes. commit-msg refuses any commit whose body lacks a context_pack_id line, binding the landed work to the exact context that was read. pre-push verifies a write-scoped session token, re-checks freshness, and rejects a push whose workspace env var points at a different repo - anti-spoof.
The sentinel everything keys on is a 329-byte JSON file with six keys:
{ "context_pack_id": "...", "context_pack_hash": "...", "owner_tool": "...",
"recall_iso": "<iso8601>", "recall_ts": <unix-float>, "workspace_path": "..." }
# stale or missing produces the same wall, with the fix inside it:
[git] REJECTED: MCP recall stale (<age>s > 1800s).
Re-run: bash <tools>/auditooor-session-start.sh
[ It is a wall, not advice. ]
Ranked by how much they helped, this recall gate is first by a wide margin: prove you pulled shared context, with a matching hash, before you can land anything. Everything else is downstream of agents being unable to skip the read. It blocked me twice today; both fixes took seconds.
[ 06 / 11 ]Boundary two: file writes
Parallel agents do not just need shared memory; they need to not destroy each other's unshared state.
The core object is a lane lease: .auditooor/agent_pathspec.json, where every active lane declares its agent_id, an intent, an expiry, and the files it owns. pre-commit-pathspec-discipline.sh - rule R36 in my numbering - refuses any commit that stages files outside the declared set. That one check ended the sweeping-git-add class of incident.
Destructive operations get their own wrapper family, R55: git-reset-safe.sh, git-checkout-safe.sh, git-clean-safe.sh and git-stash-safe.sh each chain pre-destructive-op-sibling-check.sh before the real command - wrappers, because mainline git still has no native pre-reset hook. The comment at the top of the reset wrapper has the division of labor: R36 polices the commit shape, R55 polices the destructive-op precondition.
Above both sits auditooor-universal-rule-enforce.sh, a PreToolUse hook on Edit, Bash and Agent calls. It pipes the pending action through a 56 KB classifier that emits required_rule_citations, then searches the prompt, command and edit bodies for those citations or an explicit rebuttal marker. Finding neither, it denies, listing which citations are missing and how to override. No silent edits, no silent commits.
[ 07 / 11 ]Boundary three: subagent spawn
A fresh subagent is the purest cold-start problem in the stack: no session history, no open files, no idea what its siblings already tried. So context is injected at spawn time, and the spawn is refused if injection did not happen.
The refusal half is auditooor-mcp-first-enforce.sh, a PreToolUse hook on the Agent tool. It hard-blocks any dispatch into an audit workspace whose prompt lacks an MCP recall block - it greps for vault_resume_context, a context_pack_id, or the CLI --call form - and separately blocks dispatches that skipped the spawn wrapper, where proof is an env var set by the wrapper or a spawn-log entry within the last 30 minutes.
The injection half is spawn-worker.sh, 792 lines that run before the worker exists. It registers the lane's pathspec lease, prefetches a dispatch-brief skeleton from the vault, injects a prior-lane scan and the known dead ends, optionally provisions a per-lane git worktree - sibling lanes physically cannot stomp each other from different directories - and logs lane_id and prompt_sha256 to spawn_worker_log.jsonl. It refuses to spawn for eight distinct reasons, exit codes 0 through 7. The first thing a subagent ever reads is its siblings' history.
[ 08 / 11 ]Deny-with-reason: interception as teaching
Hard blocking alone would produce an agent flailing against an opaque wall. What makes interception steer rather than merely stop is the shape of the denial. Every PreToolUse gate speaks the same JSON contract:
{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"permissionDecisionReason": "<rule> + <why> + <exact command to fix it>"
}
}
The reason string is in-context teaching: the rule, why it applies, the exact next command. A real one reads like: this dispatch targets an audit workspace but the prompt contains no MCP recall block; fix: run vault_resume_context with this argument shape, record the context_pack_id, then route through spawn-worker.sh. The correction lands inside the agent's context window at the moment of the mistake, the one place a language model reliably acts on it.
Because absolute rules breed absolute workarounds, every gate has the same escape hatch: a rebuttal marker - an HTML comment, <!-- rule-rebuttal: reason -->, the reason capped at 200 characters by the hook's regex - a shell-comment variant for raw git, or a bypass env var. Every bypass appends a JSONL row to an audit log: timestamp, wrapper, subcommand, pid, ppid, reason. You can always get past a gate. You can never get past one silently.
[ 09 / 11 ]The token math
Shared memory is usually sold on quality. The budget argument is just as strong, and it is byte math, not vibes:
[ what one recall actually costs ]One pack is roughly 1/1,600th of the tool tree it summarizes and roughly 1/55,000th of the tracked repo. At the common rule of thumb of about 4 bytes per token of English - an estimate, not a benchmark - a 34 KB pack costs 8 to 9 thousand tokens, against megabytes of re-reading to re-derive the same orientation.
The second saving is dead ends: a vault_known_dead_ends query returns at most 10 small JSONL rows and converts the 20-minute rediscovery into a skip. The third is spawn-time injection: the enriched brief replaces the open-ended exploratory reads a cold subagent performs before doing anything useful. None of this is a controlled benchmark. It does not need to be; the ratios are not close.
[ 10 / 11 ]Model-agnostic by construction
Nothing above touches a model API, and that is the design. The gates live at process boundaries: PATH order, core.hooksPath, wrapper scripts in front of CLIs, PreToolUse hooks in the agent harness. Claude Code hits them through its hooks config. Codex hits the identical 1800-second freshness gate through its own PATH shim, whose header calls its job Codex-side parity with the Claude-side enforcement surface. The kimi wrapper gates query, chat and run the same way; a dispatch preflight covers the provider path for Kimi and MiniMax workers. Session tokens carry owner as a first-class field: claude | codex | kimi | minimax | service-account.
One level deeper: the sync job compiles Claude's memory files and Codex's rules files in as named sections of the same vault, so one model's notes become the other's recallable history.
And the deny messages are plain text on stdout: any agent that can read its own tool error can be steered. That is the whole cross-model story - enforce at the boundary every process must cross, and the model in the driver's seat stops mattering.
[ 11 / 11 ]The recipe, and the honest limits
Strip away my file names and the recipe generalizes to any agent stack:
- A boring substrate you can grep: markdown plus append-only JSONL.
- A read API that returns small, hash-identified context packs instead of the corpus they summarize.
- Exactly one write path, token-gated.
- Fail-closed checks at the three places work becomes history: committing, writing files, spawning agents.
- Escape hatches that log instead of forbidding, so exceptions become data.
- A sync job that routes derived artifacts back into the substrate so the next recall returns them. Mine is
obsidian-vault-sync.py, 613 lines: an incremental compiler from sources to vault sections, expensive git-backed sections throttled to a 24-hour staleness window.
The limits, honestly. The hooks are fail-closed on policy but deliberately fail-open on plumbing: if jq is missing or stdin will not parse, the hook allows, on the stated principle that a hook bug must never break all dispatches. A crashed gate is an open gate. Bypasses get real use: this website's own repository carries a 1.3 KB bypass log from commits I approved with the recall requirement off, because it is not an audit project. Freshness is a proxy for reading: a 30-minute window plus a hash citation binds a commit to a pack, not proof the agent internalized a word of it. The index drifts: older notes say about 108 callables, my writeup said around 120, the live count as I publish is 122 - memory systems need their own maintenance. And the carrying cost is real: a 36,317-line MCP server plus 26 hook scripts and shims is infrastructure, not a weekend trick.
The vault everyone asks about predates all of the enforcement, and for most of its life it was furniture. The line I keep coming back to from my own notes: the vault did nothing until the hooks made bypassing it impossible, at every boundary, including the subagent's first breath.