building vodou.

A rule at byte 37,367 is not a rule if the cap is 32 KiB

Five AI coding hosts read five rules files in one repo. Mine had drifted for months, and the rule that mattered most sat past Codex's 32 KiB context cap.

Chad Priest / / 8 min read

Every coding agent you run reads a file before it touches your repo. Claude Code reads CLAUDE.md. Cursor reads .cursorrules and .cursor/rules/*.mdc. Copilot reads .github/copilot-instructions.md. Codex and Gemini read whatever they find in the repo root. You wrote those files at different times, for different reasons, and you have not diffed them since.

I diffed mine yesterday. The Cursor file was dated May 26. It had none of the parallel-session commit rules, none of the pre-commit guards, nothing about vendored dependencies, nothing about never fabricating tool output. Cursor was working in the same worktree as everything else, which is exactly where those rules matter.

Five hosts read five files, and four of them were wrong

The policy in question was never Claude-specific. It said things like: never git add -A in this worktree, because other sessions have in-flight edits in the shared index. Never npm install <name> inside an MCP server directory, because some dependencies are vendored file: links and naming a package forces a registry lookup that can prune them. Never fake a tool result when a skill says to call one.

All of that is project policy. It lived in a Claude-specific file because that is where whoever wrote it was sitting. Every other host got whatever happened to be true on the day someone last remembered it existed.

Nine blocks, one small adapter per host, and a check that exits 2

The shared policy now lives once, in nine numbered blocks under templates/rules/: tool priority, skills, agent actions, execution, commits, deps, lane canon, work logging, auto-memory. Each host gets a small adapter under templates/rules/hosts/ that names a target path, lists which blocks it takes, and supplies a host-specific head and tail. vodou-core rules render composes them.

This is deliberately not a template engine. Blocks concatenate in numeric order. The only real decision an adapter makes is which blocks it takes, because that is the only decision that differs between hosts. GEMINI.md takes all nine (13,500 bytes). .github/copilot-instructions.md rides every Copilot request, so it carries only the four that have to be resident: tool priority, commit rules, vendored deps, lane canon (7,527 bytes).

render --check exits 2 when any generated file differs from what its source would produce. scripts/rules-guard.py makes that a pre-commit guard, which is the point. Generation without a gate is a suggestion.

comparedcomparedtemplates/rules/NN-*.mdnine shared blockshosts/*.mdtarget + block list +head/tailrules rendergenerated filesCLAUDE.md, .cursorrules,.mdc, GEMINI.md, copilotspliced regions inAGENTS.mdhand-written manual, quotedpolicypre-commit guard--check exits 2 on anydifferenceThe guard is the feature. Rendering without it just moves the drift upstream.
One source, five readers, one gate

Three files named AGENTS.md: 65,958, 43,976 and 14,262 bytes

Then I asked whether the operating manual needed the same treatment, and the answer was worse than I expected.

There were three files named AGENTS.md in this project. The root manual, 65,958 bytes. The workspace copy that the session bootstrap actually serves, 43,976 bytes. The install seed, 14,262 bytes. Three documents, one name, and no code path that had ever compared them. Every conversation about “what AGENTS.md says” had been a conversation about a different file depending on who was speaking.

root manual65958 Bworkspace copy the bootstrap serves43976 Binstall seed14262 B
Three documents, one filename

The first design was to generate AGENTS.md from blocks like every other host file. That was wrong, and I threw it away. AGENTS.md is a hand-written manual with 64 KB of prose that is not policy: subsystem maps, architecture, troubleshooting. Generating it wholesale would mean either moving all of that into templates (absurd) or truncating the manual to the parts I had templated.

So policy is spliced, not generated. Where the manual quotes a block it does so between <!-- rules:begin 70-lane-canon --> and <!-- rules:end 70-lane-canon -->, and render fills the region in place. --check flags a region that drifted. An unknown stem or a missing end marker is an error, not an empty region, because silently blanking a region is precisely the failure this exists to prevent. After splicing, root is mirrored to the workspace copy, so the pointer every session receives names one document.

Byte 37,367, and Codex stops reading at 32,768

The finding that changed the shape of the work: Codex has no rules file of its own. It reads root AGENTS.md natively, under a 32 KiB cap.

The lane canon (the rules about parallel processes, ports and prompt-injection sites) sat at byte 37,367. Past the cap. Codex had never read it, not once, and nothing anywhere would have told me that.

Worse, I went looking for the commit rules in the manual and they were not there at all. git add -A appeared nowhere in 65,958 bytes. The parallel-session rules had only ever existed in per-host files. The manual, the document I point every agent at, had no opinion about the failure mode that has cost me two real incidents.

Both are now spliced regions near the top: commit rules at byte 948, the lane canon at 4,117. Codex’s adapter is not a file. It is placement.

before37367 B offsethost cap32768 Bafter4117 B offset
Where the lane canon sits in AGENTS.md

Two copies of a policy with no comparator is a fork with a delay

Here is the transferable property, stated so you can check it rather than agree with it.

For every span of text that exists in two files, there is a command that exits nonzero when they differ, and it runs before the commit. The lane canon had two copies. They were byte-identical the day I looked, which is why nobody had noticed, and they were guarded by nothing, which is why they would not have stayed that way.

And the second one, which I had never articulated: a rule’s effect is a function of its byte offset, not its presence. For any reader with a context cap, content past the cap is not weakly weighted. It is absent. If your rules file is longer than your host’s budget, the bottom half of that file is decoration.

Point one needle at every host file and read the offsets

This takes about three minutes on your own repo and uses nothing of mine.

FILES="CLAUDE.md AGENTS.md .cursorrules .github/copilot-instructions.md GEMINI.md .clinerules"

# 1. When did each of these last change?
for f in $FILES; do
  [ -f "$f" ] && printf '%s  %s\n' "$(git log -1 --format=%ad --date=short -- "$f")" "$f"
done

# 2. Pick the rule you would most hate an agent to violate.
NEEDLE='git add -A'
for f in $FILES; do
  [ -f "$f" ] || continue
  off=$(grep -abo -m1 -- "$NEEDLE" "$f" | cut -d: -f1)
  printf '%-10s %7s bytes  %s\n' "${off:-MISSING}" "$(wc -c < "$f")" "$f"
done

Passing looks like every existing file carrying the needle, at an offset comfortably under the smallest cap you care about, with dates clustered. Failing looks like what I got: one file three months older than the rest, MISSING in the file your most-used agent actually reads, and an offset with five digits in a file some host truncates at 32,768.

If step 2 prints MISSING anywhere, do not fix it by pasting. Pasting is how you get here. Put the text in one place, generate the rest, and add the comparison to .git/hooks/pre-commit so the next paste fails.

What the one-source generators get right, and what they skip

This problem has a small ecosystem. ai-rulez generates tool-native output for 19 platforms from one .ai-rulez/ directory. agnostic-ai makes a point of byte-stable output across runs. oneagent and groundrules both distribute one source to CLAUDE.md, AGENTS.md, Cursor, Copilot and Gemini. ds-agent-rules adds a layered base/overlay/team composition model. They are all correct about the core move, and I would not argue with any of their file layouts.

Two things I did not find in any of them.

The first is the enforcement point. Every one of these ships a generate or sync command. None of them, as documented, fails your commit when a generated file was hand-edited. That gap is the entire lifetime of the drift: someone fixes a typo in CLAUDE.md directly, it works, and the source is now behind. Byte-stable output makes the check possible. It does not make it happen.

The second is placement. Every generator treats a host as a target path plus a format. For a host that reads a shared file under a cap, the host constraint is an offset, not a path, and no amount of correct generation helps if the rule lands at byte 37,367. Sync tools solve “which file”. They do not solve “which 32 kilobytes”.

What I cannot check yet: every context cap but one

I know one cap. Codex reads root AGENTS.md under 32 KiB, so that adapter places its regions near the top and I verified the offsets by hand. I do not have a cap model. If Copilot tightens what it carries per request, or Gemini truncates at some size I have not measured, nothing in this system fails. The guard proves that generated bytes match their source. It has no opinion about whether the host got to the end.

Two smaller ones. The decision was that project policy stays project-scoped, so there is no global ~/.claude/CLAUDE.md adapter and nothing here reaches another repository, which means a second checkout of a different project gets none of this. And swapping the Codex and Gemini binaries mid-test left stuck PIDs twice and needed a reboot to clear, which is not related to the rules work at all, but it is what the afternoon actually cost.