DEV Community

Penloom Studio
Penloom Studio

Posted on

The CLAUDE.md sections that actually matter (and the ones wasting your context)

I wrote a linter that counts instructions in CLAUDE.md files, and running it against my own projects taught me something uncomfortable: the problem was never that my files were too short. Every single one had plenty of content. The problem was that most of that content was doing nothing — or worse, actively crowding out the ten lines that mattered.

"Keep it short" is the advice everyone repeats now, and it's correct. Anthropic's own best-practices doc says to keep it concise and human-readable, target under 200 lines, and warns that longer files consume more context and reduce adherence. HumanLayer, whose engineering blog has one of the better write-ups on this, keeps their root CLAUDE.md under 60 lines.

But "short" is a constraint, not a plan. The real question is: which lines earn a spot in the file, and which ones are paying rent they can't afford?

Here's the test I now apply to every line: would a competent new hire need this on day one, and would they be unable to infer it from the code? If either answer is no, the line goes.

That test sorts everything in a CLAUDE.md into two piles.


The six sections that earn their place

1. Commands — exact, copy-pasteable

The single highest-value content in any CLAUDE.md. Claude cannot infer that your test runner needs a flag, or that npm test is broken and everyone actually runs npm run test:fast.

## Commands
- Build: `pnpm build` (NOT npm — lockfile is pnpm)
- Test single file: `pnpm vitest run path/to/file.test.ts`
- Typecheck: `pnpm tsc --noEmit` — run after every change
- DB migrations: `pnpm drizzle-kit push` (dev only, never in prod)
Enter fullscreen mode Exit fullscreen mode

Four lines. Notice each one carries a non-obvious detail. pnpm build alone is inferable from the lockfile; "NOT npm" prevents a real failure mode.

2. The architecture map — where things live

Three to six lines that answer "where do I look?" — not a directory listing (Claude can run ls), but the parts that carry intent:

## Layout
- `src/core/` — pure business logic, no I/O, no framework imports
- `src/adapters/` — all external calls (DB, APIs) live here, nowhere else
- `legacy/` — frozen. Read for reference, never modify.
- Generated: `src/gen/**` — never edit by hand, run `pnpm codegen`
Enter fullscreen mode Exit fullscreen mode

The legacy/ and src/gen/ lines are boundary markers. In my experience these prevent more damage than any style rule in the file — an agent that edits a generated file produces a change that silently reverts on the next codegen run, which is a genuinely miserable bug to trace.

3. Conventions a linter does NOT enforce

This is where most files go wrong in both directions. The rule of thumb from the official guidance is right: never duplicate what a linter already enforces. If ESLint or Prettier will catch it, the line is pure waste — Claude Code sees the lint failure and fixes it anyway.

What belongs here is the stuff with no automated enforcement:

## Conventions
- Errors: return `Result<T, E>` from core functions; `throw` only at adapter boundaries
- New endpoints follow the pattern in `src/api/users.ts` — copy it
- Feature flags: check `flags.ts`, never read env vars directly in components
Enter fullscreen mode Exit fullscreen mode

Note the second line: pointing at an exemplar file is dramatically cheaper than describing the pattern in prose. One line of pointer replaces thirty lines of explanation, and the exemplar can't drift out of date the way prose does.

4. Verification — how Claude proves its work

Claude Code is significantly more reliable when it can check its own output. Tell it how:

## Verifying changes
- `pnpm tsc --noEmit && pnpm vitest run` must pass before you finish
- UI changes: `pnpm dev` runs on :3000; screenshot before claiming done
Enter fullscreen mode Exit fullscreen mode

Without this section, the agent decides for itself what "done" means. With it, you've defined done.

5. The short "never" list — with reasons

Hard boundaries, kept brutally short — and every rule gets a why, for the generalization reasons I laid out in the instruction-budget post:

## Never
- Never commit directly to `main` (branch protection will reject the push anyway)
- Never touch `*.generated.ts` (regenerated on build; edits are silently lost)
- Never add a dependency without asking (bundle budget is 250 KB, we're at 238)
Enter fullscreen mode Exit fullscreen mode

That last line is the pattern to copy: the reason ("we're at 238") lets the model make a correct judgment call on the case you didn't write a rule for.

6. Pointers to deeper docs — progressive disclosure

Both Anthropic and HumanLayer converge on the same mechanism for everything that doesn't fit: don't paste the detail, point to it, and Claude pulls the file only when the task calls for it. I covered the mechanism in the instruction-budget post, so here I'll just show the shape:

## More detail (read only when relevant)
- Testing philosophy and fixtures: `docs/testing.md`
- Release process: `docs/release.md`
- DB schema decisions: `docs/adr/003-schema.md`
Enter fullscreen mode Exit fullscreen mode

The sections quietly wasting your context

Everything below fails the day-one-hire test. I've seen every one of these in the wild — several in my own files:

  • The project mission statement. Three paragraphs on what the app does and who it serves. Claude needs one sentence, and mostly needs it never.
  • Pasted API documentation. The docs for your framework are in the model's training data or one WebFetch away. Fifty lines of pasted Drizzle docs is fifty lines of pure tax.
  • Style rules your tooling enforces. "Use 2-space indent." Prettier does this. Delete.
  • The tutorial. Step-by-step "how to add a feature" walkthroughs that duplicate what an exemplar file shows for free.
  • The changelog. "2025-11: migrated to App Router." History belongs in git.
  • Generic engineering wisdom. "Write clean, maintainable code with good names." This instructs nothing. Every model already attempts this; the line spends budget on zero information.

The insidious part is that none of these lines look harmful individually. But the file is loaded into every session, and I made the case in the instruction-budget post linked above that adherence degrades as instruction count climbs — the model doesn't error on rule #212, it just quietly stops following some of them. The filler doesn't merely cost tokens; it competes with your real rules for attention.


What this looks like on a real file

Here's the actual output from running claude-md-lint on one of my own project files today (diagnostic portion):

claude-md-lint  CLAUDE.md
────────────────────────────────────────────────
Instruction budget score: 🟢 84/100
Instructions counted: 56  (soft 150 / hard 200)

 • Within budget: 56 instructions (target ≤ 150).
 • 46 rule(s) appear to give no REASON (heuristic). A rule with a "why"
   generalizes to unseen cases; a bare command doesn't. Add "— so that …".
 • 3 "must-happen" rule(s) (always/never/must). A prose rule lands ~80% of
   the time; a deterministic hook fires ~100%. Graduate the critical ones
   into hooks.
Enter fullscreen mode Exit fullscreen mode

Look at that middle finding: the file is comfortably within budget, and still 46 of its 56 rules are bare commands with no reason attached. Length was never this file's problem — information density was. That's exactly the failure mode the six sections above are designed against.

A before/after (composite, built from real files)

Before — a "conventions" section assembled from lines I keep finding when I lint these files. No single file I've linted is quite this bad, but every line below is one I've seen in a real file (a few of them in my own):

## Code Style
We care deeply about code quality. Always write clean, readable code.
Use TypeScript for all new files. Use meaningful variable names.
Follow the existing patterns in the codebase. Use 2-space indentation.
Prefer const over let. Use async/await instead of raw promises.
Write JSDoc comments for exported functions. Keep functions small.
Always handle errors appropriately. Use early returns to reduce nesting.
Prefer functional patterns where it makes sense. Avoid any.
Make sure imports are sorted. Remove unused imports before committing.
Enter fullscreen mode Exit fullscreen mode

That's eight lines carrying sixteen rules. Run the day-one-hire test: TypeScript is inferable (every file is .ts), indentation and import sorting are Prettier/ESLint's job, and more than half of what's left is generic wisdom. What survives:

## Conventions
- `noUncheckedIndexedAccess` is on — index access returns `T | undefined`, handle it
- Exported functions in `src/core/` get JSDoc (docs site generates from them)
Enter fullscreen mode Exit fullscreen mode

Sixteen rules down to two — and the two survivors are ones the model would actually get wrong without being told. That's the trade every time: the short version isn't a summary of the long version, it's the residue after you remove everything the model already knows or your tools already enforce.


The 60-second audit

Open your CLAUDE.md and score each line:

  1. Could Claude infer this from the code or lockfiles? → delete
  2. Does a linter/formatter already enforce it? → delete
  3. Is it generic advice with no project-specific content? → delete
  4. Is it detail needed for <20% of tasks? → move to a docs/ file, leave a pointer
  5. Is it a "must-happen" rule? → keep, add the reason in parentheses
  6. Is it a command, boundary, or exemplar pointer? → keep, these are the file

Most files I've run this on lose half their length and none of their function.


I keep the full checklist, plus the reliability rules I apply before shipping any agent, in the free Claude Code Field Guide: penloomstudio.com/field-guide.html

Top comments (0)