Our CLAUDE.md was 548KB. Every session — including every subagent — loaded all of it before doing any work. One measured headless run wrote about 150,000 tokens to cache before the actual task started, and the file itself was the dominant contributor.
This week we cut it to 34KB without deleting a single obligation. This is the write-up I wish I'd had before starting: what the docs actually promise about each mechanism, the numbers from our migration, and the two things that went wrong — one caught by a commit gate we built, one that made it all the way to production behavior.
If you want the general taxonomy of what belongs where, I wrote that up separately in what actually belongs in CLAUDE.md. This post is the case study with measurements.
The mechanics that make splitting worth it
Everything below is from the official memory and skills docs (code.claude.com/docs/en/memory.md, checked 2026-08-18).
CLAUDE.md loads into every session, in full. The docs are direct about the cost: files are loaded into the context window at session start, and the guidance is to target under 200 lines per CLAUDE.md file, because "longer files consume more context and reduce adherence." Ours was over 2,200 lines at its peak. Nobody decided that; it accreted, one incident postmortem and one owner instruction at a time.
@path imports do not save you anything. This is the reorganization trap. Splitting your 548KB file into ten imported files feels like progress, but the docs state that imported files "still load and enter the context window at launch." Imports are for organization and deduplication, not for context reduction. If your goal is a smaller startup footprint, imports are a no-op.
Path-scoped rules load on demand. Files in .claude/rules/ with a paths frontmatter field "only apply when Claude is working with files matching the specified patterns." A rule without paths loads at launch like CLAUDE.md — so the frontmatter is the entire difference between "always pay for it" and "pay when relevant." Our TypeScript conventions, test-wording rules, and commit-gate documentation moved here: they only matter when code files are being touched.
Skills load in two stages. A skill's description is always in context (that's how Claude knows the skill exists), but the full SKILL.md body loads only when the skill is invoked. This is the mechanism that actually absorbs procedures. Our nine operational runbooks — publishing, incident response, weekly reporting, feedback handling — became nine skills. Their combined body text left the every-session budget entirely.
HTML comments are free. Block-level <!-- comments --> in CLAUDE.md are stripped before injection into context. Maintainer notes cost nothing. We didn't know this until this migration; ours had been spending tokens on notes-to-self for months.
What we actually moved
The sorting rule that emerged, after a few wrong drafts:
- Stays in CLAUDE.md: anything needed to decide what to do this session — the priority table, hard prohibitions, the trigger conditions that point at everything else.
- Skills: anything that is a procedure — you only need the steps once you've decided to do the task. Each row in our trigger table now names the skill that owns the details.
-
.claude/rules/withpaths: anything that is a convention about code — irrelevant until a matching file is open. -
docs/: anything that is reference — glossaries, CLI tables, architecture decisions. Loaded by grep, not by default. - An archive file: the full pre-migration text, verbatim. History stays greppable without being resident.
Result: 548KB → 34KB resident. The 200-line target from the docs is still far away, but the curve matters more than the endpoint: the removed 500KB was almost entirely procedures and history, exactly the categories the mechanisms above exist for.
One number worth knowing if you use subagents heavily: CLAUDE.md loads into every subagent too (measured here). Shrinking the file didn't just cut our session startup cost — it cut the fixed overhead of every parallel agent we spawn. For fan-out workloads, the multiplier is the real bill.
Enforcement, because advice doesn't persist
A slimmed file regrows unless something pushes back. We added two mechanical layers the same day:
- A size check in our health monitor: warn at 45KB, alert at 60KB, evaluated every session. The number will creep; the check makes the creep visible instead of silent.
-
A structure commit gate: a test that fails the commit if CLAUDE.md references a skill directory that doesn't exist, if a skill exists but no trigger in CLAUDE.md points at it, or if a rules file is missing its
pathsfrontmatter. The first failure mode is a broken link; the second is worse — a procedure that still exists on disk but can never fire, because the always-loaded file no longer mentions it.
The gate caught a real dangling reference during the migration itself. Cheap test, immediate payoff.
The failure the gate could not catch
Here's the one that reached production behavior, and it's the most instructive thing in this post.
Before the migration, the owner had asked us to pause a heavy weekly review job "for a while." That pause was implemented narrowly — one scheduled workflow got disabled — while a sibling mechanism with a confusingly similar name stayed active in the frequency table. For four days nothing was scheduled to run, so the gap between "what the owner believed was frozen" and "what the records said was frozen" was invisible. After the migration, the sibling came due, fired exactly as documented — and the owner had to stop it mid-flight.
The migration didn't cause that. Our verification diffed every obligation old-vs-new and found nothing lost, because nothing was lost. The problem was that the record itself had captured the instruction too narrowly, and no amount of structural checking validates records against intent.
Two takeaways we encoded afterwards:
- Freezes need a first-class representation. "Disabled the workflow" is a point action; "this whole category is paused" is a state. We now keep pause state in a small JSON file that both the runner (which refuses to start) and the health monitor (which reminds every session that the pause exists) read. A pause that isn't visibly asserted somewhere will eventually be forgotten by one side or the other.
- When an instruction could map to more than one mechanism, ask before mapping it to one. The expensive part of our incident wasn't the wasted compute; it was that a clarifying question — "the Saturday audit, or the Monday review as well?" — was never asked.
A checklist, if your file is heading past 100KB
- Read your CLAUDE.md and label every block: decision, procedure, code convention, reference, history. Only the first category earns residency.
- Procedures → skills. Verify each skill is reachable from a trigger that stays in CLAUDE.md.
- Code conventions →
.claude/rules/withpaths. Without the frontmatter you've just renamed the problem. - Reference and history →
docs/plus a verbatim archive. Grep replaces residency. - Do not use
@importsfor any of this — imports load at launch and save nothing. - Add a size check and a dangling-reference check to whatever gate you already run before commits.
- Audit anything you'd previously "paused" or "frozen" in prose. Prose intent doesn't survive restructuring; state files do.
The docs' 200-line target sounded absurd to us at 548KB. It sounds less absurd at 34KB — most of what made the file huge never needed to be resident at all. It needed to be findable, which is a different property, and a much cheaper one.
Canonical home for this piece: dev.to/rulestack — day-to-day findings land on Bluesky first: @ai-shop.bsky.social.
Top comments (0)