My AI Hygiene Rule Passed Its Own Check For Months. The Check Was Blind.
My AI Hygiene Rule Passed Its Own Check For Months. The Check Was Blind.
The short version
- I built a four-letter command,
:save, that runs a documented cleanup at the end of every AI session. Biggest win in my AI workflow, full stop. - Its safety check was
wc -lagainst a 300-line threshold. It reported a comfortable yellow for months on a file that was 49,758 characters — because one table row held 14,765 of them, and a table row is one line. - Measure context files in characters (
wc -c), and add a guard that no single line exceeds ~1,500 chars. - A stale save procedure in a sub-folder can win by proximity — silently, because it looks authoritative.
- One save command was doing two jobs. Writing the day’s detail and writing the durable digest have opposite requirements. Splitting them — plus rotation, and a metric that could see — cut the documentation corpus 1.45 MB → 294 KB, and the part that loads on every prompt by 47% — zero facts lost.
- The rule you can steal in one line: each fact gets exactly one home.
$ cat the-command.txt
The Four Letters
I have a four-letter command that’s been the single biggest win for my AI workflow. It isn’t in any docs. I made it up.
:save
Type it at the end of a session and the AI runs a documented cleanup, leaving the project in a state where the next session — me tomorrow, or a fresh AI booted up cold — can pick up clean. Without it, my projects rotted from the inside.
This article was supposed to be a victory lap. Then the command’s safety check turned out to be blind, and I had to go find out what else it hadn’t been telling me.
$ cat the-original-sin.txt
What :save Replaced
Before :save, my end-of-session ritual was a paragraph of casual instruction: “update the CLAUDE.md and the CHANGELOG with what we did, mention the new credential, fix any documentation that’s now wrong.”
That paragraph had a bug in it. The bug is “update the CLAUDE.md AND the CHANGELOG.” Both files. Same content. Every session.
CLAUDE.md auto-loads on every single prompt — durable rules, conventions. CHANGELOG.md is the running session journal. Two jobs, two lifetimes. Telling an AI to update both with the same content is a duplication trap, and an AI will comply. Cheerfully. Forever.
My rule said update both files, so both files grew, every session, forever.
I didn’t catch it for four months. Then one morning I ran /context and watched the AI report that 38,600 tokens of every session were being eaten by documentation that was supposed to help me work — not by the work.
The fix took an afternoon and produced the rule everything else here descends from:
Session narrative goes to the changelog.CLAUDE.md is for durable rules, constants, and pointers.
That rule was right. It’s still right. It just wasn’t enough — and the check I wrote to enforce it couldn’t tell.
$ cat the-blind-check.txt
Step 0 Was The Load-Bearing Step. Step 0 Was Lying.
Here’s the health check I shipped, recommended, and put in a draft of this very article as the thing you should steal:
0. Pre-flight health check. Run `wc -l CLAUDE.md`.
- <=300 lines -> healthy, continue.
- 301-500 -> surface to user. Don't add prose.
- >500 -> refuse. Propose a split.
Green, proceed. Yellow, be careful. Red, stop and split. Clean. Mechanical. Impossible to argue with.
It reported 305 lines for months. Barely over. A yellow I’d been comfortably living in.
Same file. Same moment. One of these gauges was the one I’d been reading.
It isn’t a bug in the threshold. 300 was a fine number. It’s a bug in the unit. wc -l counts newlines. Markdown tables, long pointer rows, and “Last Updated” banners are where content goes when it doesn’t want to be counted. The check wasn’t lenient — it was structurally incapable of seeing the thing it was built to see.
The proof arrived when I started cleaning: the first rewrite removed 20,499 characters and the line count did not improve. Twenty thousand characters of bloat, invisible to the metric, gone — and the gauge didn’t move.
Then I checked a second project’s file with the corrected metric. 293 lines — it would have passed the old check comfortably — and 100,171 characters. Line 4, the “Last Updated” field, was 28,850 characters. A changelog had been stuffed into a date field.
The “Last Updated” field is where changelogs go to hide.
The fix, which takes about a minute
Measure in characters, then go find your longest line — that second check is the one that actually matters, because a file can be perfectly sized and still hide a whole changelog in one row:
wc -c CLAUDE.md
awk '{print length, FNR}' CLAUDE.md | sort -rn | head -5
If any single line is over ~1,500 characters, open it. That’s where your changelog is hiding.
And while I’m confessing: the line-count check at least ran. My other file — the one holding current state, read first every session — had a stated target of “under 400 tokens” and had never once met it, on any day, since the target was written. I’d written a number that felt responsible, never measured against it, and let its presence in the file stand in for the discipline it described.
The arithmetic on that one, and why it’s the worse failure
The file was 50,082 characters. A ~400-token target is roughly 1,600 characters — so it ran about 31× over a limit that had never been checked in the same units it was written in.
That’s the more embarrassing of the two failures. The blind check was at least checkable. This one was decoration.
A threshold nobody can meet is functionally identical to no threshold — but worse, because it manufactures false assurance. Both are the same disease: a control you never watched fail is not a control. If you have never seen your check catch something, you don’t know that it can.
$ cat the-hard-one.txt
The Rule That Won Because It Was Closest
This one is most likely to be sitting in your setup right now.
A sub-project folder had its own CLAUDE.md containing its own save procedure — written months earlier, before every rule in this article existed. Because my system followed the nearest definition, that stale copy silently won every save run from that directory.
Nobody chose it. It won by proximity.
Its changelog reached 332 KB with no rotation, while the project root’s rules said rotate at 40 KB. And the whole time it looked completely authoritative — because it was a CLAUDE.md, in the right place, with the right heading.
A stale local procedure is invisible precisely because it looks authoritative.
A full sweep found five of these. Three were in the folder where I write these blog posts. One instructed an AI to write “any new server details, credentials, or configuration” into a documentation file that had no business holding any of it.
The fix was a policy, not a patch: only a project root may define a save procedure. A sub-folder can declare local facts — which changelog file it uses, where its archives go — but never the workflow. If a sub-folder defines one, ignore it, keep walking up, and report it as drift.
The guard that failed silently was the guard against silent failures. Invoking a skill with arguments caused the loader to substitute my argument text into an awk variable named $0. The check rendered as invalid awk — and produced no error. The file on disk was correct. Only the rendered version was corrupted, which means no amount of reading the file would ever have found it. I caught it by chance, watching the skill’s own output as it loaded. A second skill had the identical bug — and then the linter I wrote to sweep for it caught a third my manual sweep had missed, because I’d grepped for $0 and this one was $1.
The broader pattern: eleven times in one day, the test was broken, not the system
By the end of the cleanup the count was eighteen. Bad greps, mismatched units, arguments starting with - being parsed as flags.
But the taxonomy is the lesson: most broken tests are false positives — they cry wolf, and a human re-checks. The dangerous one is the false negative. A secret-scanner with an invalid regex printed “0 secrets found”, which is indistinguishable from a clean pass.
Nobody re-checks good news. That is the only kind that ships a defect.
$ cat the-real-shape.txt
Everything Appended. Nothing Subtracted.
With a metric that could actually see, the real problem showed up immediately — and it wasn’t any single file.
Every path in my system appended. The save command appended to five different files. Every hook appended. Every habit appended. And there was, in the entire vault, no subtractive tool of any kind.
A system where every path adds and no path removes will bloat at exactly the rate it is used — and it bloats fastest on the days you do the most work, which are the days context matters most.
I hadn’t built a documentation system with a discipline problem. I’d built a ratchet, and been surprised that it only turned one way.
The numbers, once measured honestly:
| File | Before | After | Change |
|---|---|---|---|
CLAUDE.md (always loaded) | 49,758 | 26,238 | −47% |
| Current-state file (always loaded) | 50,082 | 26,535 | −47% |
CHANGELOG.md | 1,246,502 | 225,816 | −82% |
A side project’s CLAUDE.md | 100,171 | 15,705 | −84% |
| Total across these four files | 1,446,513 | 294,294 | −80% |
| — of which loads on every prompt | 99,840 | 52,773 | −47% |
No facts were deleted. The changelog’s 274 entries came out the far side as 47 active + 227 archived = 274, zero lost. The rest moved into reference files that load on demand — free until the moment the AI actually needs them.
The two files at the top of that table are the ones that load on every prompt, of every session. Halving them is the line that actually shows up in a context window; the rest is a corpus that had simply stopped being navigable.
What was actually in there — the drift markers
I counted what had accumulated in the file whose one job is durable rules:
- 30 git commit hashes
- 67 dates
- 40 status emoji
- 23 DONE / LIVE / BUILT / SHIPPED markers
A file governing how an AI works on my projects had quietly become a status board — in direct violation of the cardinal rule printed inside it. After the cleanup: 30 commit hashes → 0. Dates 67 → 33 (a rule’s ratification date is durable; a task’s completion date is not).
Commit hashes are the tell. If your always-loaded context file contains a git hash, it is doing a job git already does, better, for free.
$ cat the-split.txt
One Command Was Doing Two Jobs
At the end of a session there are two completely different things you want written down, and I had been asking one command to do both:
1 — Everything that happened today
Every fact learned, decision made, thing left open. High volume, only useful for a while. This should be append-only — rewriting is where facts die.
2 — The durable digest
What a future session actually needs: current state, standing rules, pointers. Low volume, long-lived, and it must be rewritten every time, because stale current-state is worse than none.
Opposite requirements. Append-only versus rewrite-in-place. Grows forever versus stays small. One command trying to serve both will fail at one of them — and it failed at the second, quietly, because rewriting is harder than appending, and an AI under a token budget will always take the easier path.
/save-session — the detail tier. A structured, append-only entry in a dated file, one per project per day. It audits the whole conversation, oldest to newest, specifically to defeat the AI’s habit of over-weighting whatever we discussed last. Run it mid-session and it becomes compaction insurance.
/save-full — the digest tier. The enforced version of typing :save. It requires a session-log entry to exist first, runs the size checks, updates the durable files, and reconciles what I raised during the session against what actually shipped. That last step is the one that catches me out.
Each fact gets exactly one home.
Detail duplicated across tiers is what reintroduces drift.
Why the session logs are dated files that never get archived
The first design had one growing session log per project, with a rotation rule at 40k. It lasted four days. The reason it died is a good one:
“We’ll have an idea when it was written, which ones we want to read, and it will self-regulate.”
Rotation requires a decision every time a file crosses a threshold — friction, which means it gets deferred under time pressure. Not hypothetical: a project’s log hit 60k+ on the very day this was being discussed and got left un-rotated, deliberately, to save the tokens that pausing to rotate would have cost.
A dated filename removes the decision entirely. One day’s work is whatever one day’s work is — self-bounding by construction. So the rule is now absolute: session logs have no size threshold and no rotation, ever. One of mine is 115 KB. That’s fine. It’s one day.
$ cat the-counterintuitive-bit.txt
Sometimes The Threshold Is The Problem
My current-state file kept losing things. My read was: the archiving is sloppy, tighten it up.
I was wrong. Every relocation checked out — verified lossless, each with five to seven inbound pointers. The damage was coming from the squeeze itself. The file ran at 92% of its 35,000-character limit, so every save, the cleanup pass had to find room that wasn’t there. It cost me three real defects: the “current focus” section — the first thing every session reads — sat empty for a full day; a stale next-step sent us hunting a scheduled job that didn’t exist; and a correction got applied to one file but not its sibling.
So I raised the limit by 29% — deliberately, alongside a scheduled cleanup rather than instead of one — and shipped a counter-rule with it:
Prefer removing a genuinely closed topic over compressing live detail. Under-pruning costs context. Over-pruning costs facts. Facts are worse. If nothing is truly closed, say the file is at its limit and let the human decide — do not invent room.
Every miss that week came from a cleanup pass finding room where there was none. An AI asked to get a file under a number will always get it under the number. What it gives up to get there is the part you have to specify.
$ cat steal-this.txt
What You Should Steal
1 — Measure in characters, and find your longest line
The two commands above, on every always-loaded AI context file you have. Anything over ~1,500 characters on one line is hiding something.
2 — Grep it for git commit hashes
If there are any, your durable-rules file is doing git’s job. Same test for dates, status emoji, and the words DONE / SHIPPED / LIVE.
3 — Split the save into two commands
Today’s detail goes to a dated, append-only file, never rotated. The durable digest gets rewritten and stays small. They have opposite requirements; one command will fail at one of them.
4 — Check for a competing procedure in a sub-folder
Search your whole tree for files defining a save or documentation workflow. If more than one exists, the one closest to where you happen to be working is silently winning. Only the project root gets to define the workflow.
$ exit
What This Series Was About
The other three articles set up problems — giving an AI real work, making sure that work has a name on it that isn’t yours, and the silent failure that destroys context mid-task. :save is what makes those three sustainable: all of them depend on documentation that is current, honest, and small enough to load.
A note on what I don’t know yet
The two-tier split is a few weeks old and I’m running it everywhere. It works. But nothing in my system verifies that a fact wasn’t written into three tiers at once — the de-duplication check I’d need to prove the one-home rule doesn’t exist yet. So that rule is currently enforced by discipline, which is exactly the thing this article argues doesn’t work.
And the honest capstone: the tracker file I declared authoritative for this cleanup’s status was itself four sessions out of date by the end. The work had been recorded correctly in three other places — just not in the one file whose job was to know. A file that nothing owns falls behind, and the fix is a step in a procedure, not a resolution to be better.
If you’ve run this discipline longer than I have and caught patterns I haven’t, I want to hear about them.
Four letters.
One colon.
Measured in characters.
