Chapter 7 of 7 · 9 min read
An imperative with concrete nouns is still a preference
On this page
The standard was adopted in February and it was a genuine improvement on nothing. Every entry proposed for the config file had to be phrased as an imperative with a concrete noun, which is objective, takes a reviewer four seconds, and requires no knowledge of what the entry is for. Forty-one entries were proposed between February and August. Forty-one were accepted. Here is a representative sample of what the file now contains:
- Always prefer composition over inheritance in `packages/billing`.
- Use constructor injection for all services in `packages/ledger`.
- Name test files after the module under test.
- Avoid premature abstraction in the `ledger` domain.
- Keep controller functions in `src/routes` under 40 lines.
- Handle `StripeError` explicitly rather than catching broadly.Every line is an imperative. Every line names a concrete noun, several of them naming real paths. Every line passed review from a reviewer applying the standard faithfully. Nobody in the team can name a task on which any of the six changed a decision, and when the paired-run harness was pointed at three of them on constructed fixtures, all three came back with identical behaviour in both arms.
The objection is right, and it predicts specific things
A senior engineer's warning that any standard will be gamed — that people will phrase preferences as invariants to get them past review — describes exactly what happened, and the mechanism is worth stating precisely. A grammatical filter is a property of the string. Attributability is a relation between the entry, a task, and what the model does. No condition on the string can imply a property of that relation, so any filter over phrasing is satisfiable by writing a different string with the same content, and writing a different string is free.
What that predicts is observable, which makes it worth writing down before you adopt any standard so you can check whether your own is working.
Preferences acquire a scope to look repo-specific. "Prefer composition over inheritance" becomes "prefer composition over inheritance in packages/billing", which cites a real path and carries exactly the same content, since the qualifier restricts nothing the model was going to do anyway.
Preferences acquire the grammar of invariants. "Don't over-abstract the ledger" becomes "the ledger domain must not contain abstract base classes", which has a truth value and is checkable in principle. Nothing checks it, no task turns on it, and it will read as the strongest line in the file to anyone skimming.
Entries cite a real incident and state a lesson much broader than the incident. Three weeks were genuinely lost to the Charge class hierarchy in February; the entry that came out of it governs inheritance across the whole repository forever.
And the acceptance rate goes to one. A standard that rejected nothing in six months and 41 proposals is not filtering; the reviewers were doing their job, and their job was checking grammar.
None of this requires anyone to be acting in bad faith, which is the part that makes the objection sharper than it first sounds. Nobody on that team was smuggling preferences past a filter. Each of the 41 authors had a real experience, wanted the next person to avoid it, saw a file whose stated purpose was recording things the team knows, and phrased their entry the way the standard asked. Gaming here is the ordinary result of a rule that measures something adjacent to what it cares about, and the people applying it most conscientiously produce the worst outcome, because conscientious application of a grammatical rule is exactly forty-one accepted entries.
The objection also implies a limit on the fix. No standard creates the willingness to reject a colleague's line, and a standard that depends on reviewers having that willingness will fail in the third month when the author is the tech lead. What a standard can do is shift the burden, so that rejecting is the default outcome of a missing field rather than a judgment about the entry's merit. "Field 2 is empty" is something a junior reviewer can say to a principal engineer; "I don't think this is valuable" is not, and designing around that asymmetry is most of what makes review standards work anywhere.
A standard phrased over evidence
Move the requirement off the entry and onto the pull request that adds it. The reviewer's question stops being "is this well-formed" and becomes "did the author show me the thing".
<!-- .github/PULL_REQUEST_TEMPLATE/config-entry.md -->
## Adding or changing an entry in CLAUDE.md
**1. The task.** Name one runnable task in this repository where this entry
changes a decision. A ticket title and the file it touches, not a category.
**2. Today's behaviour.** Paste the trace excerpt showing what the agent does
on that task without this entry. A prediction is not evidence.
**3. The check.** What in this repository fails if this entry stops being true?
(CI assertion, lint rule, path check, trace predicate.) If nothing can, say so
and say why.
**4. The location.** Why does this belong in text rather than in the runner, a
harness guard, or a CI gate? Reversible if ignored? Visible in review?
**5. The budget.** The file is at N/1000 tokens. If adding puts it over, name
the entry that comes out and why this one is worth more.Field 2 is what a grammatical filter can never ask for, and it is the field that stops the gaming, because a trace showing the agent already doing the right thing is an argument against the entry that the author has to produce themselves. Field 3 connects each entry to the machinery from the fourth chapter, so that an accepted entry arrives with its own staleness alarm. Field 4 routes the bottom-right cell of the previous chapter's table out of the file before it becomes a sentence someone trusts.
The standard needs one more clause, stated in the reviewer's own words so they can quote it: a correct principle with no failing trace is not admissible in this file. That is the clause reviewers will find hardest to apply, and it is worth naming the exact case in the document, because it will arrive dressed well. A respected engineer proposes "prefer composition over inheritance in packages/billing" and attaches the February incident. The principle is true. The incident is real. The author's judgment is sound and their scar is genuine. Rejecting the line feels like rejecting all of that, which is why every reviewer in the world accepts it.
The way to make the rejection possible is to give it somewhere to go. A rejected entry is redirected rather than discarded, and the redirect is usually better than the config file would have been:
| What was proposed | Where it goes instead | Why |
|---|---|---|
| "Prefer composition over inheritance" | The style guide, and an architecture decision record (ADR) about the Charge hierarchy | It is a human-facing belief; the model already complies |
"The ledger domain must not contain abstract base classes" | An ESLint rule with an error message pointing at the ADR | Checkable, so check it, on every commit rather than probabilistically |
| "Keep controller functions under 40 lines" | A lint rule if the team wants it, otherwise nowhere | Neither the agent nor review needs it stated in prose |
"Handle StripeError explicitly" | Stays, if a trace shows the agent currently catching broadly | This one might have field 2 behind it; the trace decides |
What catches an entry that survives review
Review is a gate applied once, against a claim about the future, by people who want to say yes. Something has to look backwards, and the mechanism is an audit that re-runs the evidence the entry was admitted on.
Each entry carries an identifier and a pointer to the fixture that justified it, which is cheap to store next to the file:
# .config-entries.yaml
- id: CE-014
line: "Run one test file: `pnpm vitest run <path>`"
fixture: prompts/proration-bug.txt
predicate: tests/agent/predicates/scoped_test_command.py
admitted: 2025-03-02
last_verified: 2025-09-30
last_effect: "5/5 vs 0/5"
- id: CE-021
line: "`billing` must not import from `internal/experiments`"
fixture: prompts/webhook-backoff.txt
predicate: tests/agent/predicates/no_experiments_import.py
admitted: 2025-05-11
last_verified: 2025-09-30
last_effect: "5/5 vs 1/5"Once a quarter, run every fixture in both arms. On this repository that is 22 entries at 5 runs per arm, 220 sessions, about four hours of wall clock across four workers and roughly $40 of model spend — small enough that the reason it does not happen is always ownership rather than cost, which is why the file needs a CODEOWNERS line and the audit needs a name against it in the calendar.
The first audit on this team's file retired six of the 22 entries, and the reasons are more interesting than the count. One entry had never worked and had been admitted on a prediction rather than a trace, from before the standard existed. Two had been overtaken by the repository: the directory they described had been reorganised, so the entry was pointing at a layout that no longer needed explaining. Three had been overtaken by the model. The scoped-test-command entry, which produced a clean 5/5-versus-0/5 split in March, produced 5/5 versus 4/5 in September, because the newer model reads package.json and the workspace layout and picks a scoped invocation on its own.
That last category is the part of this subject nobody has solved. Entries decay because the model improves, with no change to the repository, no failing check, and no incident to prompt a re-read. A config file is a set of corrections to a moving baseline, and a correction that was worth 3,000 tokens in March can be worth nothing by September while looking exactly as sensible as it did when it was written. Nothing observable inside a session distinguishes those two states, so the only instrument is the periodic ablation, and the only thing that makes teams run it is having made it somebody's job.
Between audits there is a passive signal that costs nothing to collect, which is the failure history of the checks from field 3. An entry whose CI assertion has fired at least once in the last year is demonstrably guarding something people still get wrong. An entry whose assertion has never fired in twelve months is either protecting against something that stopped happening or duplicating a guarantee that something else now provides, and either way it belongs at the front of the next audit's queue rather than being deleted on that evidence alone. Sorting the audit by that signal means the four hours are spent on the entries most likely to be retired.
The standard has a cost, and pretending otherwise is how it gets abandoned. Producing field 2 means running the fixture twice and pasting a trace, which takes about twenty minutes for someone who has the harness set up and most of an afternoon for someone who does not. That cost is the mechanism rather than a side effect: it means entries are added by people willing to spend twenty minutes establishing that the entry does something, and it prices out the drive-by addition that produced most of the 41. It also means the harness has to be a maintained piece of team infrastructure with a documented invocation, because a standard that requires evidence nobody can cheaply produce becomes a standard that gets waived.
Two limitations of the audit are worth being explicit about rather than discovering. It cannot distinguish an entry that prevents a rare catastrophe from an entry that does nothing, since both show no split on any fixture you can safely run. Entries in that class should be exempted from ablation by name and given the previous chapter's review instead — asked whether they should still be text at all, rather than whether they show an effect. And the audit measures each entry alone, so it will not catch the case where twelve individually-attributable entries are collectively diluting each other, which is what the budget exists to prevent.
Starting on your own repository
The order that works on an existing file is roughly the order of this topic. Read the file end to end, which most teams have not done in a year. Run the repeated-noun sweep for conflicts. For each entry, try to name a fixture; the entries with no constructible fixture go first, and they will be a third of the file. Set a budget and put it in CI next to the command extractor, so that growth and staleness both become failures rather than drift. Move anything in the irreversible-and-invisible cell into the runner or the harness, leaving one sentence behind that explains the guard. Then adopt the evidence standard for additions, and put the first audit in the calendar before the file has had time to grow back.
Starting from nothing is easier, and worth doing from a template rather than a blank page — the collected claude-md-templates and HumanLayer's Writing a good CLAUDE.md both begin from the commands-and-invariants shape rather than the values-and-preferences shape, which saves a team the six months it usually takes to discover the difference. Whichever way you start, the first version should be short enough that every line has a task behind it, because the standard the file keeps for its whole life is the one set on the day it was created.
Agent configuration files (AGENTS.md, CLAUDE.md and equivalents) — read the current version at https://daviddominguez.dev/learn/en/agent-configuration-files-agents-md-claude-md-and/an-imperative-with-concrete-nouns-is-still-a-preference