~/posts / agent-docs-rot
The rules you wrote in CLAUDE.md quietly peel away from the code
●
For people running Claude Code with a CLAUDE.md. I went through the setup of five personal projects in one pass, and what was broken was not the code — it was the rules. All four kinds of breakage, too, were the kind that never raises an error.
Here is the conclusion up front. Rules peel away from the implementation. You cannot stop the peeling itself, so the only move left is to add a mechanism — a test — that notices when it has happened.
Context
- As of July 2026. Claude Code desktop, Windows 11
- Five personal repositories: a statically exported Next.js site, a Python desktop app, and others
- Written for people who already have a
CLAUDE.mdand have let it grow a bit
If your CLAUDE.md is still a few hundred lines, none of this has happened to you yet. It starts to bite about half a year in.
The rules disagree with the implementation
On a statically exported Next.js site, I rewrote the rules document from the current code. Three errors in the rules fell out.
| What the rules said | What was actually true |
|---|---|
Put export const dynamic = "force-static" on every page |
Not one of the 46 files under src/app/**/page.tsx has it. With output: "export" every route is static anyway, so it is unnecessary |
Write metadata in page.tsx |
Pages are almost all "use client", so they cannot export it. In practice the sibling layout.tsx holds it (40 files) |
pnpm test runs every test |
vitest has three projects. One of them boots Playwright and Chromium |
The first one was the dangerous one. Had I followed that rule and set a SEO subagent loose on an “audit for missing declarations”, all 46 pages would have gained a meaningless line. The tests pass. The build passes. The diff review ends with “yes, matches the rules”.
When a human reads a rule and the code in front of them disagrees with it, they stop. An AI does not stop. The moment you hand a rules document to an AI it stops being prose and becomes an executable specification — and yet nobody attaches anything that keeps it in sync with the implementation.
The third one had a side effect, too. I had left a note about a workaround — “storybook tests fail depending on the environment, so exclude them” — and that workaround was dragging the worker project down with it. The workaround itself had rotted.
Nobody can see the volume
On another project, a Python desktop app, the problem was not the content but the size.
2026-07-21 29KB
2026-07-27 55KB (+90%)
Nearly double in six days. And 68% of the growth was a single section, “design decisions”. The result of appending measurements and records of failures to CLAUDE.md as they came up.
CLAUDE.md is loaded whole at the head of every session. It eats a fixed slice of context every single time.
The problem is that the structure makes the growth invisible. A +2KB commit looks perfectly reasonable each time, and each individual addition genuinely was correct. A diff review only shows the increment; nobody sees the total. Correct additions piling up until the whole thing is broken is, I think, exactly the failure mode review is structurally bad at.
Bind the budget with a test
After splitting it up and getting back down to 26KB, I turned the size ceiling into a test so that the next time this happens, something notices.
# Ceiling against the 26.2KB right after the split. Room to grow, but it stops
# before the file doubles.
CLAUDE_MD_BUDGET = 34_000
def test_claude_md_stays_within_budget():
size = len(CLAUDE_MD.read_bytes())
assert size <= CLAUDE_MD_BUDGET, (
f"CLAUDE.md is {size} bytes, over the {CLAUDE_MD_BUDGET} budget. "
"Do not delete lines — move measurements and records of failures "
"to docs/design-rationale.md."
)
The part I put thought into is the assert message. Told only that they went over, people delete lines. What gets deleted is usually the most important part: why things are the way they are. So the escape hatch is written down in advance.
The split itself took some care as well. Bold text in CLAUDE.md is used as a “here be dragons” marker, so I mechanically matched all 179 bold spans before and after the split and confirmed not one had been dropped. Exactly one had vanished in a rewording, and it went back in. At that count, checking by eye would have missed it for certain.
Bind the dangling references too
I added things like this to the same test file.
- Do the slash commands
CLAUDE.mdpoints at actually exist under.claude/commands/? - Do the hook scripts referenced by
settings.jsonactually exist? - Are the sections that grew in the rationale documents actually linked from
CLAUDE.md?
All of these are references where nothing happens when they break. An agent told about a command that does not exist quietly falls back to doing the work by hand. An unlinked section is read by nobody, and the rule walks off on its own. If a hook path breaks, the rule quietly reverts to not being enforced.
Configuration meant for AI does not throw when it breaks. That is what decisively separates it from ordinary code.
A hook that had never once run
Here is that “quietly reverts to not being enforced”, lived out. A PostToolUse hook was supposed to run prettier on save: read the JSON on stdin with jq, format with pnpm dlx prettier, and finish with || true.
Except this machine does not have jq installed. The pnpm launcher is broken too. In other words this hook had, in all likelihood, never formatted anything since the day it was configured. || true swallowed the failure, and the status line still showed up on every run, so it looked like it was working.
Worse, the pre-completion checklist command said “the hook always formats, so format is out of scope”. Nobody at all was actually responsible for formatting.
Three fixes.
- A hook must be self-contained within the repository’s dependencies and Node. Read the JSON on stdin with Node, not
jq - Drop the
|| true. A PostToolUse exit 1 is non-blocking (the tool call still succeeds) and its stderr is visible to the user, so exit 1 beats being silently disabled - When you write “some other mechanism guarantees this”, confirm that mechanism actually runs before you write it
Right after the fix, even the tables in the existing CLAUDE.md got reformatted. Which is itself the evidence of how long it had not been working.
Split the storage by role
The other thing that worked was separating where things get written. Put everything in one file and nobody can tell which part to update, so it gets left alone.
| Where | What goes there |
|---|---|
docs/rules/ |
Intent you cannot read off the code, constraints that break things when removed, decisions that turned out wrong in the past |
docs/knowledge/ |
Reproducible traps (this external site returns 403, this approach comes up empty, and so on) |
| The agent’s memory | Machine-specific things. pnpm is broken here, that sort of thing |
On top of that, the first principle on the rules side became “do not write down what reading this repository would tell you”. No general TypeScript or React etiquette either. That alone shrinks the volume on its own.
One thing I felt strongly here: creating a new file should be the last resort. Add a file every time you learn something and none of them get read. Appending to a location that is already genuinely being referenced survives far better.
An aside: for a while the rules lived in a git submodule called .agent/. The remote it pointed at disappeared, the submodule was removed along with it, and the entire contents were lost. There was no backup. And since .gitignore still lists .agent/, writing them back in the same place would lose them again. “Do not put your rules in a gitignored directory” looks like a joke until you walk straight into it.
The next step
If you want to be suspicious of your own CLAUDE.md, this is the order I would take.
- Measure the byte count first.
wc -c CLAUDE.mdis enough. It will be bigger than you think - Write that number into a test as the ceiling. Put the escape hatch in the assert message
- Assert that the commands and hook paths
CLAUDE.mdpoints at actually exist - Finally, pick the one rule most likely to rewrite a lot of files if you had it audited, and count whether the current code actually follows it
If even one disagreement falls out of step 4, be suspicious of the rest. I got three.
Everyone gets as far as writing the rules. The mechanism that detects the written rules peeling away from the implementation is, unlike tests, something nobody sets up for you. If you are going to have an AI read them, that part comes with it.