There is a moment when a CLAUDE.md tips over. That moment does not sit on any calendar, it shows in behavior: the file keeps getting longer, and the rules inside it keep getting followed less. Every new convention you write in dilutes the ones already there. That is exactly where the project behind this article stood — and the answer was not a better CLAUDE.md, but its dissolution into individual rule files. Today there are 27 of them.
This article is the experience report: how Claude Code rules need to be structured so they hold up over months, what a single rule file must carry, and why the most important parts are not the rules themselves. They are the don’ts and the inventories.
The key points up front:
- One file per convention — 27 rule files instead of one CLAUDE.md, each with a single topic.
- Anatomy of a rule that holds: rule, reasoning, don’ts, inventory, cross-references.
- Don’ts beat prescriptions — at least in the DI² project: a negative example is concrete and recognizable, a prescription competes against the training prior.
- Inventories are the drift radar: the part of a rule that claims an actual state of the code — and therefore the first to stand out when code and documentation diverge.
paths:scoping lets large rules load only where they apply — 3 of the 27 files use it.- The honest downside: maintenance effort, rule conflicts, and a state in which the documentation deliberately runs ahead of the code.
Prerequisites. A project with Claude Code and a .claude/rules/ directory. The pattern transfers to any coding agent that loads convention files into its context — Cursor rules or comparable mechanisms in other tools work on the same principle. What loads into context when, and what that costs, is covered by the sibling article Skills vs. Rules in Claude Code — this article starts one level earlier: at the question of what the rule files themselves have to look like.
Contents
- The Starting Point: One File That Kept Growing
- Anatomy of a Rule That Holds
- Why Don’ts Beat Prescriptions
- Inventories as a Drift Radar
- Rules That Load Only Where They Apply
- The Honest Downside
- When a Rule Needs a Linter
- What I Would Do Differently Today
- FAQ
- Related Articles
The Starting Point: One File That Kept Growing
The project behind the numbers is DI², an ETL generator built on Next.js and PostgreSQL, its code written almost entirely AI-assisted with Claude Code. In the beginning, all conventions lived where every Claude Code project collects them first: in the CLAUDE.md. That works as long as the file is short. It stops working once the file becomes a container in which database conventions, color tokens and commit rules all sit side by side.
This effect is hard to measure, but it is easy to feel. It also matches what research shows about long contexts: language models make measurably worse use of information sitting in the middle of long inputs (“Lost in the Middle”, Liu et al. 2023). In my experience, a rule sitting in line 40 of a long file gets followed less than the same rule in its own, topically named file. A second effect has less to do with the model than with the humans: in a 400-line file, even the author cannot find a rule again when they want to check whether it still holds. Why a single file does not scale structurally, and which loading mechanics sit behind that, belongs to the skills-vs-rules article — this one is about what comes after.
The consequence in the project: a .claude/rules/ directory holding 27 rule files, one per convention (as of July 22, 2026). The same count holds for the public edition of this rule structure, the di2-starter-kit on GitHub — you can verify it there, not counting the subfolder READMEs. There is a file for the table conventions, one for dialogs, one for loading states, one for the security model. The directory keeps growing with the project. In the very week this article was written, three new files arrived, for views, triggers and database policies. A rule system is finished when the project is finished, which is to say never.
Anatomy of a Rule That Holds
After several months with this system, a fixed structure has emerged. A rule file that holds consists of five parts:
1: # <Convention> (<project>)
2:
3: > One-sentence summary: What does this rule enforce, and where does it apply?
4:
5: ## The Rule
6:
7: Every <structure/component/procedure> <does exactly one thing, stated imperatively>.
8: One convention per file — do not mix topics.
9:
10: ## Reasoning
11:
12: Why this rule exists: the concrete incident, bug or review finding
13: that triggered it. A rule without a reason gets weighed away in
14: trade-offs — the reason is part of the rule, not decoration.
15:
16: ## Don'ts
17:
18: - ❌ `<concrete negative example from your own code>` — why it drifts.
19: - ❌ `<second negative example>` — what applies instead (with the target spelling).
20:
21: ## Inventory
22:
23: | Usage site | File | Status |
24: |---|---|---|
25: | <Site A> | `components/site-a.tsx` | ✅ compliant |
26: | <Site B> | `components/site-b.tsx` | ⏳ retrofit open |
27:
28: ## Cross-References
29:
30: - [neighbor-rule.md](neighbor-rule.md) — boundary: what is governed there, not here.
The rule itself (lines 5 to 8) is the smallest part, and that is no accident. It states imperatively what applies, in two or three sentences. Everything beyond that belongs to the other four parts.
The reasoning (lines 10 to 14) is not a courtesy to the reader. An agent weighs trade-offs, and as observed in the DI² project, a rule without a reason loses that weighing more easily against a plausible counterargument from the concrete case. A rule with a reason anchors what the concrete case has to argue against. The difference shows in exactly the moments that matter: namely, when the model considers an exception justified. How a convention and its reasoning come into being in the first place is described in the methodology article Deriving SQL Conventions with Claude Code.
The remaining three parts are the actual substance of this article. The don’ts and the inventory each get their own section below, and the cross-references almost explain themselves: they draw the boundary to the neighboring rule so that two files do not creep into governing the same topic. Every boundary violation that surfaces later gets recorded there as an explicit reference.
Why Don’ts Beat Prescriptions
This is the central observation from several months of rule maintenance in the DI² project. Whether it transfers to other projects has not been examined — it is project experience, not a study. A prescription says what should be. A negative example shows what must not be — with a concrete, recognizable spelling. The difference looks small at first glance. In practice it is not:
1: <!-- Before: a prescription without an anchor -->
2:
3: Use only the project tokens for font sizes.
4:
5: <!-- After: negative examples with recognition value -->
6:
7: **Don'ts:**
8:
9: - ❌ `text-[12px]` — raw pixel value, drifts; snap target is `text-token-meta`.
10: - ❌ `text-sm` — framework default instead of the project scale; in the app
11: scope the linter guard flags it as an error.
12: - ❌ Inline `line-height` override via `[line-height:Xpx]` — the scale ships
13: its own line height; a deliberate override needs a code comment with
14: a justification.
The prescription in line 3 is factually correct, and yet it accomplishes little. At every generation it competes against the model’s training prior, in which text-sm is the statistically most common way to write small text. “Only project tokens” first has to be translated onto the concrete case, and the rule gets lost in that translation.
The negative example skips the translation. text-[12px] is precisely the string the model is about to write — it sits verbatim in the rule, marked with a ❌ and the reason. A don’t leaves no room for interpretation. It additionally names the snap target, the spelling that applies instead. Whoever reads the don’t, whether human or model, afterwards knows both things: what is wrong and what belongs in its place. There is no don’t-specific magic behind this, but a familiar effect from prompt research: concrete examples are easier for language models to act on than abstract prescriptions. Negative examples are simply the form in which a rule file can harness that effect.
The best don’ts do not come from the rule author’s imagination but from real finds. Every time a review or a bug surfaces a new bypass variant, exactly that variant goes into the rule file as a don’t. The rule system thereby learns the same mistakes the code has already made once. There was no shortage of material: the sibling article AI-Assisted Coding Gave Me 799 Hardcoded Font Sizes documents the drift finding from which the font-size don’ts emerged.
Inventories as a Drift Radar
A rule states a target state, and target states have an inconvenient property: they cannot become wrong. “Every dialog carries a leading icon” stays correct as a sentence even when six dialogs without icons have long been sitting in the code. The rule notices none of it.
An inventory changes that. It lists the concrete usage sites of the convention together with their actual state:
1: ## Inventory + Retrofit Backlog
2:
3: As of now, this rule is the documented truth. Existing sites without
4: <convention> are brought in line as a tracked follow-up step (retrofit) —
5: until then the code deliberately lags behind the documentation.
6:
7: | Dialog | File | Status |
8: |---|---|---|
9: | Editor (domain object A) | `components/object-a-editor-dialog.tsx` | ✅ icon leads left |
10: | Inspector (domain object B) | `components/object-b-inspector.tsx` | ✅ icon leads left |
11: | Invite user | `components/user-invite-dialog.tsx` | ⏳ retrofit open |
12: | Bulk delete | `components/bulk-delete-dialogs.tsx` | ⏳ retrofit open (destructive tone) |
The table is the part of a rule file that can fail against the actual state of the code. A reasoning section or a cross-reference can go stale too, but only the inventory makes a checkable claim about what the code looks like right now. If a new dialog arrives and is missing from the table, the inventory is incomplete. If a listed dialog gets rebuilt and its status is not updated, the inventory is stale. Exactly this vulnerability is what makes it valuable: a rule without an inventory can exist past the code unnoticed for years, while the lag of a rule with an inventory becomes visible at the next reconciliation, at the latest. The inventory is the rule’s drift radar.
In the project, 9 of the 27 rule files carry such sections. They come in two flavors. The caller inventory lists who uses a component or convention — it answers the question “if I change this, what is affected?” before anyone has to search. The retrofit backlog lists which existing sites do not yet satisfy the convention. Both forms share the mechanics but differ in the direction of view: one looks at the rule’s users, the other at its open debts.
The inventory is not maintained in a separate documentation session but in the same commit as the code change. Whoever adds a dialog adds it to the table. Whoever completes a retrofit sets the status to ✅. That sounds like a high demand on discipline. In an agent workflow, though, it is the cheapest possible moment, because the agent usually has the rule file in context anyway when it works inside the rule’s scope.
Rules That Load Only Where They Apply
As the file count grows, the context-cost question returns. 27 files that all load all the time would just be a partitioned CLAUDE.md at the same cost. The lever against that is a paths: front matter that binds a rule to its path scope:
1: ---
2: paths:
3: - "src/app/api/**"
4: - "src/lib/db*"
5: ---
6:
7: # Backend conventions
8:
9: This rule loads only when the task touches files in its path scope —
10: API routes and the database access layer. A frontend task does not
11: pay its context costs.
In the project, 3 of the 27 files carry this front matter: the backend rules, the frontend rules and the security model. The selection follows a simple criterion. These three files are large, and their scope is a clearly bounded subtree of the project. A backend rule inside a pure frontend task is dead context. The remaining 24 files load unscoped because they are either small or apply across the whole project, like the commit conventions.
This article deliberately goes no deeper here. The mechanics behind it — what Claude Code loads into context when, what separates rules from skills, and how the costs add up — are the subject of Skills vs. Rules in Claude Code. For the structural question of this article, the finding suffices: paths: scoping is the main reason 27 files do not mean 27-fold costs.
The Honest Downside
A rule system of this size is not free, and an experience report that hides that would be advertising.
Maintenance is real work. Every rule file wants attention at every convention change, inventories want updating in the same commit, and the cross-references between files go stale when a rule moves. The effort is not a one-time investment but a running cost. In the project it is the kind of work that pays off, but it does not disappear just because you approve of it.
Rules end up in conflict. With 27 files it happens that two rules govern the same case from different angles — the dialog rule wants an icon, the confirmation-dialog rule forbids one for its special case. The resolution is the same every time: the conflict gets written into both files as an explicit carve-out, with references to each other. Undecided conflicts are the worst thing that can happen to a rule system, because then the prioritization stays ambiguous — which rule prevails depends on the particular context and the task at hand.
The documentation runs ahead of the code — deliberately. When a new convention is decided, it applies to new code immediately. The existing sites are not rebuilt within the hour but tracked as a retrofit backlog in the inventory and brought in line step by step. That state is not a failure as long as it is documented. The rule file says honestly: this is the truth, and the code deliberately lags behind at these listed sites. Untracked, the same state would be a lie, because then the documentation claims an actual state that does not exist.
When a Rule Needs a Linter
Prose has a limit, and the sibling article AI-Assisted Coding Gave Me 799 Hardcoded Font Sizes has measured it out in detail: a documented rule raises the generation hit rate but does not guarantee it. At high volume, any residual rate turns into visible drift.
From that follows a division of labor that has proven itself in the project. Whatever is machine-checkable gets a guard in addition to the rule — in the font-size case a custom ESLint rule at error level that flags exactly the spellings from the don’ts. The rule file remains the source all the same: it explains the why, defines the mapping and lists the deliberate exceptions, while the linter enforces only the checkable subset.
Whatever is not machine-checkable stays a pure prose rule and needs the inventory as its substitute radar. No linter can decide whether a dialog icon is the right one for the domain. Whether all dialogs have one is written in the inventory table. The rule of thumb from the project: a rule whose violation can be expressed as a search pattern is a linter candidate. A rule whose violation a human has to recognize needs an inventory.
What I Would Do Differently Today
Looking back, I would approach three things differently, and all three can be named concretely.
Too many files too early. The first weeks produced rule files for topics that did not even have a second usage site yet. A convention for a single case is not a convention but a note. Today a new rule file comes into being only when the same decision comes up for the second time. And cleanup is part of the deal: a rule whose scope has disappeared, or whose content a neighboring rule meanwhile carries, gets deleted or merged into that neighbor. A rule set that only ever grows becomes the very unwieldy file it was meant to replace.
Rules without inventories that went stale silently. The early files consisted of rule and reasoning, without a state-of-the-code part. After two months, some of them described a state the code had long left behind, and nobody had noticed. Only the inventory sections made the aging visible. In hindsight, every rule with usage sites should have carried one from day one.
Discovered paths: too late. The scoping arrived only when the context costs were already noticeable, and converting existing files to clean path scopes was more tedious than an early cut would have been. Anyone starting today should ask one short question at every new rule file: does this apply everywhere, or in one subtree? Answering that question costs ten seconds at creation time. Not asking it costs a refactor later.
FAQ
At the latest when, while writing a new convention into it, the question comes up where it actually belongs. That is the signal that the file carries multiple topics. A second signal is the repeated ignoring of a rule that verifiably sits in the file — the rule is drowning in its surroundings.
The number itself is not the limit, the context costs are. 27 files work because the large ones load path-scoped and the unscoped ones are small. How those costs add up and where the line runs is covered by Skills vs. Rules in Claude Code.
Because they close the translation gap. A prescription first has to be applied by the model onto the concrete case, while a don’t already contains the wrong spelling verbatim — precisely the string the training prior would suggest, marked with the reason and the target spelling. Recognizing is more reliable than deriving.
Write the conflict into both files, as an explicit carve-out with references to each other. Which rule wins in the overlap case must be stated in the files, not in the author’s head. An undecided conflict otherwise gets decided by chance, depending on which rule happens to sit more prominently in context.
No — and that may be the most counterintuitive lesson. A new convention applies to new code immediately, while the existing code is brought in line step by step as a tracked retrofit backlog. The tracking is what matters: a documented lag is a deliberate decision, an undocumented one is documentation that lies.
Related Articles
Going deeper:
- Skills vs. Rules in Claude Code — What Auto-Loads, What Loads on Demand — the loading mechanics and context costs behind this article.
- Maximal Template Over Empty Repo — a Claude Code Setup That Prunes Itself via /init — how a rule inventory gets tailored at project start.
- Setting Up a Claude Code Project with a Development Workflow and Database — the sub-pillar: the setup this rule system lives in.
Upstream:
- AI-Assisted Coding Gave Me 799 Hardcoded Font Sizes — the drift finding that makes rules and guards necessary in the first place.
- Deriving SQL Conventions with Claude Code — the Generate-Refine-Derive Loop — how a convention comes into being before you pour it into a rule file.
- AI-Assisted SQL Development with Claude Code — Rules, Skills and Agents — the pillar: the enforcement system as a whole.
Starter kit:
- The open DI² starter kit on GitHub — a project template built on this article’s one-file-per-convention principle, ready to tailor.