Claude Code treats your CLAUDE.md as context, not enforced configuration — the official memory docs say so in exactly those words. Every line you write is a request the model weighs, not a rule it obeys. Most files fail because they are written like wikis: project history, style essays, duplicated README content, aspirational advice.
Wikis get skimmed. We treat CLAUDE.md as a different document type: a contract. Few clauses, each specific, each verifiable, each with an enforcement story — and a maintenance loop that proves the clauses still hold.
What you’ll learn
- Diagnose why Claude Code skims past instructions in a long CLAUDE.md
- Rewrite vague guidance into short, verifiable contract clauses
- Cut the always-loaded surface with path-scoped rules in
.claude/rules/ - Escalate merge-blocking rules out of prose and into PreToolUse hooks
- Audit which instructions actually load and which actually change behavior
Prerequisites
- A project you run Claude Code in daily, with an existing CLAUDE.md you suspect is being ignored
- A recent Claude Code build —
.claude/rules/discovery,/memory, and theInstructionsLoadedhook all appear below - Working knowledge of hooks and
.claude/settings.json
Why Claude Code skims your wiki
The memory docs state the mechanism plainly: CLAUDE.md content “is delivered as a user message after the system prompt, not as part of the system prompt itself.” Claude reads it and tries to follow it, but the docs add there is “no guarantee of strict compliance, especially for vague or conflicting instructions.” Every clause is context competing for attention against your actual task.
There is also a budget, and the official best-practices page is blunt about it: “Bloated CLAUDE.md files cause Claude to ignore your actual instructions!”
The docs set the target at under 200 lines per file. Longer files hurt twice: adherence drops, and every line is billed to your window on every session — part of the standing overhead covered in context budgeting in Claude Code.
So the failure mode is not that Claude Code is broken. It is that a 300-line wiki asks for 300 things at once, and attention is finite.
A contract asks for twelve things, each checkable. That is the entire strategy.
CLAUDE.md best practices as contract clauses
The docs give the standard for a clause that survives: “write instructions that are concrete enough to verify.” Their own examples set the bar — “Use 2-space indentation” instead of “Format code properly”; “Run npm test before committing” instead of “Test your changes.”
A clause you cannot verify is not a clause. It is a preamble, and it costs tokens while changing nothing.
Here is the rewrite move applied to a typical wiki paragraph:
## Code quality We care a lot about clean, maintainable code. Please write idiomatic TypeScript and try to keep everything nicely formatted. Testing is really important to us, so make sure your changes are properly tested before you finish. Also please keep the API layer organized and consistent.
- Run pnpm lint:fix after every edit (formatting lives in the linter config, not here) - Run pnpm test --filter <changed-package> before calling a task done; the full suite takes 20+ minutes - API handlers live in src/api/handlers/, one file per resource
The before version reads as values. The after version reads as checks: each line names a command, a path, or a rule Claude either followed or did not. “Write clean code” belongs to the category the model already attempts by default — it consumes budget without shifting behavior.
Three more levers from the docs govern what gets followed. Consistency: “if two rules contradict each other, Claude may pick one arbitrarily,” so conflicts between root CLAUDE.md, nested files, and rules are a documented failure mode.
Structure: headers and bullets beat dense paragraphs. Emphasis: if Claude keeps skipping one clause, the best-practices page suggests adding “IMPORTANT” to that line alone. Emphasize many lines and none of them stands out.
The official include list is short: bash commands Claude cannot guess, style rules that differ from defaults, repo etiquette, architectural decisions, environment quirks, non-obvious gotchas. The exclude list is the wiki: anything Claude can learn by reading code, standard language conventions, API documentation, frequently-changing details, file-by-file codebase tours, and self-evident advice.
For every surviving line, apply the documented pruning test: would removing this cause Claude to make mistakes? If not, cut it.
Add a clause only on evidence: Claude makes the same mistake a second time, or a review catches something it should have known. Typing the same correction you typed last session also counts. Repeated corrections are pure waste — that cost is the subject of the correction tax in agentic coding.
And keep task-scoped constraints out of the permanent contract entirely; those belong in the prompt or spec, as covered in spec-first prompting for Claude Code.
Shrink the always-loaded surface
A contract stays short because most material lives somewhere cheaper. Claude Code concatenates instruction files rather than overriding them.
Content loads from the filesystem root down to your working directory, and user-level rules load before project rules. CLAUDE.local.md is appended after CLAUDE.md at each level. Instructions closer to where you launched Claude are read last.
The .claude/rules/ directory is the main relocation target. Rules are markdown files discovered recursively, one topic per file — but the frontmatter decides whether the move saves anything.
A rule without paths frontmatter loads at launch with the same priority as .claude/CLAUDE.md: better maintainability, identical context cost. A rule with paths loads only when Claude reads a file matching the glob:
---
paths:
- "src/api/**/*.{ts,tsx}"
---
# API conventions
- Handlers live in src/api/handlers/, one file per resource
- Validate request bodies with the zod schemas in src/api/schemas/
- Never return raw database errors; wrap them in ApiError
from src/api/errors.ts
Fifteen lines of API conventions now cost nothing in sessions that never touch src/api/. Path scoping is the one placement move that reduces the startup bill instead of reorganizing it.
Imports do not slim the contract either. The @path/to/file syntax expands at launch (max depth of four hops), and the docs warn that imported files “still load and enter the context window at launch.” Unscoped rules and imports organize; only paths frontmatter defers cost.
Two more placement notes. In monorepos, subdirectory CLAUDE.md files lazy-load when Claude reads files in those directories, and claudeMdExcludes in settings skips irrelevant ancestor files. The full pattern is in scoping Claude Code in monorepos.
And rules are for constraints, not procedures: a multi-step release runbook belongs in a skill that loads on invocation, not in an always-on rule. When you are unsure which layer an instruction belongs to, use the decision matrix in choosing the right extension point.
Escalate merge-blockers into hooks
Some clauses should not be prose at all. Anthropic’s steering post draws the line exactly: “When there’s something that absolutely must not happen, an instruction is the wrong tool.”
Under pressure, deep into a long session, or facing a prompt injection in a file it just read, the model can fail to follow a prompted rule. The same post names the alternative: “A real guardrail needs to be deterministic, and the enforcement methods are hooks and permissions.”
The working heuristic: if violating the rule would block a merge in CI, it belongs in a hook or permission rule. If violating it would merely raise a reviewer’s eyebrow, it can stay in CLAUDE.md.
Say your contract contains - NEVER edit files in migrations/ by hand, and you have watched Claude violate it once in a long session. That second occurrence is the trigger to escalate. The best-practices page notes that Claude can write the enforcement for you, with a prompt like “Write a hook that blocks writes to the migrations folder.”
Whether Claude drafts it or you do, review it before you commit it. It should have this shape: a PreToolUse matcher on the file-editing tools, plus a script that denies matching paths:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": ".claude/hooks/block-migrations.sh"
}
]
}
]
}
}
#!/usr/bin/env bash
# Deny Edit/Write to migrations/. Exit 2 = block, and stderr
# is fed back to Claude so it can self-correct.
file_path=$(jq -r '.tool_input.file_path // empty')
if [[ "$file_path" == *"/migrations/"* ]]; then
echo "Blocked: migrations/ is generated." \
"Run pnpm db:migrate instead." >&2
exit 2
fi
exit 0
Exit code 2 blocks the action, and the stderr message reaches Claude — so the next attempt gets redirected to pnpm db:migrate instead of silently failing.
Then comes the step most people skip: delete the NEVER edit migrations/ line from CLAUDE.md. The rule is now enforced, so the prose is dead weight. The contract shrinks as enforcement hardens.
Exit code 1 does NOT block — it signals a non-blocking error, and the tool call proceeds. A guardrail script that exits 1 on violations silently waves everything through. After wiring any blocking hook, test it by asking Claude to perform the forbidden action.
Style rules deserve the same demotion in the other direction. Never send an LLM to do a linter’s job: formatting and lint rules belong in the formatter, wired to a PostToolUse hook, not in prose the model re-litigates every session. For deny decisions via JSON output, Stop-hook gates, and the full enforcement toolbox, see hooks as guardrails, not suggestions.
Audit the contract
A contract you never audit drifts back into a wiki. The docs prescribe a real debugging ladder for “Claude isn’t following my CLAUDE.md” — run it as a ritual after a few working sessions, not on day one.
- 01
Confirm the file is loaded at all
Run
/contextand check the Memory files list. That is where the memory docs send you to see which CLAUDE.md and rules files actually loaded into the current session. (/memoryis the editing view: it lists memory file locations and opens them, but it is not a record of what loaded.)If the rule you are arguing about is not in the list, it was never in the room. The usual causes: a path-scoped rule whose glob never matched, an excluded monorepo file, or a nested file that has not lazy-loaded yet.
- 02
Log loads systematically with InstructionsLoaded
One-off checks miss intermittent problems. The
InstructionsLoadedhook fires whenever an instruction file enters context — at session start and on lazy loads. It has no blocking power; it exists for observability.Wire it to append to a log file, and “did my path-scoped rule ever trigger this week?” becomes a grep. Pair it with the telemetry patterns in instrument your Claude Code usage.
- 03
Make Claude grade its own adherence
At the end of a real working session, paste the audit prompt:
Read ./CLAUDE.md. For each instruction, output a table with three columns: (1) the instruction, (2) whether you followed it in this session — cite the specific action or diff as evidence, and (3) classify it as: BEHAVIOR-CHANGING (you’d have acted differently without it), REDUNDANT (you’d have done this anyway), or UNVERIFIABLE (too vague to check). Don’t be diplomatic.
What you are after is a triage table: “Run
npm testbefore committing” marked followed, with the command cited as evidence; “write clean, maintainable code” flagged UNVERIFIABLE; linter duplicates flagged REDUNDANT. Everything not marked BEHAVIOR-CHANGING is a deletion candidate under the pruning test. Treat the grades as triage, not ground truth — a model scoring its own compliance is a heuristic, which is why steps 1 and 4 exist. - 04
Watch for the two documented symptoms
The docs name two behavioral signals.
If Claude keeps doing something you banned, the file is probably too long and the rule is getting lost. If Claude asks questions your CLAUDE.md already answers, the phrasing is probably ambiguous. The first symptom means delete or escalate; the second means tighten the wording.
- 05
Prune, then re-test by observing behavior
Apply the deletions and rewrites, then treat CLAUDE.md like code: test the change by watching whether behavior actually shifts over the next few sessions. Two caveats while you observe.
After compaction, the project-root CLAUDE.md is re-read from disk, but nested CLAUDE.md files are not re-injected until Claude touches their directories again. A rule that “stopped working” mid-session may just live in the wrong file — see surviving auto-compact in Claude Code.
And check
/memoryfor auto memory overlap: Claude records its own per-repo learnings inMEMORY.md, so clauses duplicating what it already learned are wasted budget.
A worked contract
Here is what the end state looks like for a mid-size TypeScript monorepo — a root file in the neighborhood of 40 lines, with everything else relocated:
# acme-api — instructions
<!-- Contract, not wiki. Every line must pass the pruning
test. Merge-blocking rules live in hooks, procedures
in skills, area conventions in path-scoped rules. -->
## Commands
- Build: `pnpm build` (turbo). Never run `tsc` directly.
- Test: `pnpm test --filter <package>`. The full suite
takes 20+ minutes; do not run it unprompted.
- DB schema changes: `pnpm db:migrate` only.
## Architecture decisions
- API handlers live in src/api/handlers/, one file per
resource.
- Errors cross the API boundary only as ApiError
(src/api/errors.ts).
- Feature flags come from src/flags.ts. Never read
process.env directly in app code.
## Repo etiquette
- Conventional commits: feat:, fix:, docs:, chore:.
- Never push to main. Branch and open a PR.
- IMPORTANT: PR descriptions link the tracking ticket or
state why none exists.
## Environment quirks
- `pnpm test` needs Docker running (testcontainers).
Check Docker before debugging "failing" tests.
- CI runs Node 22. Do not commit lockfile changes
generated under other Node versions.
<!-- Removed 2026-07: "NEVER edit migrations/" — enforced
by .claude/hooks/block-migrations.sh since the hook
landed. "Release process" — moved to the release
skill. Style rules — enforced by lint hook. -->
Every surviving clause is a command Claude cannot guess, a decision that differs from defaults, etiquette, or an environment trap. Nothing here restates the linter, tours the codebase, or asks for virtue. The removed lines did not vanish — they moved to layers with better economics:
.claude/
├── rules/
│ ├── api.md # paths: src/api/** — loads on touch
│ ├── frontend.md # paths: apps/web/**/*.tsx
│ └── reviews.md # unscoped — always loaded, kept tiny
├── hooks/
│ └── block-migrations.sh # the ex-NEVER clause
├── skills/
│ └── release/SKILL.md # the 70-line runbook
└── settings.json
Block-level HTML comments in CLAUDE.md are stripped before injection. Annotate the contract for humans — why a clause exists, what was removed and where it went — at a cost of zero tokens. The changelog comment in the example above is free.
The maintenance loop is now closed — and the loop, not the file, is what makes CLAUDE.md best practices stick.
- New failure observed twice: add a clause
- Clause turns out to be merge-blocking: escalate it to a hook and delete the prose
- Clause only matters in one directory: scope it with
paths - Audit says UNVERIFIABLE or REDUNDANT: cut it
The file stays a contract because every line keeps having to re-earn its slot.
On a team, the loop is what usually breaks first. Each engineer patches the shared CLAUDE.md with their own corrections, nobody runs the audit, and within a quarter every repo has a different wiki with a different set of ignored rules. When we help teams through Enable, we standardise the contract shape, the escalation path into hooks and the audit ritual across repos, so a clause means the same thing wherever an engineer launches Claude Code.
Next steps
- Hooks as guardrails, not suggestions — the full enforcement layer that merge-blocking clauses escalate into
- Context budgeting in Claude Code — where CLAUDE.md sits in the startup bill, and the other levers that shrink it
- Choosing the right extension point — the decision matrix for CLAUDE.md vs rules vs skills vs hooks
External references worth bookmarking: the official memory documentation for the complete loading mechanics, and Anthropic’s steering Claude Code post for the context-cost comparison across every steering surface.