CLAUDE.md conditional rules: there is no syntax for them.
You came here because you wrote a heading that says ## When working in /api and you have a hunch the rules under it are not actually scoped. They are not. The harness does not parse those headings. The model reads them; the file ships intact, every turn.
This page covers what conditional rules look like when people try to write them in CLAUDE.md, why they fire anyway, and the three places they should live instead.
1. Why no inline conditional exists, from the source
Open src/lib/analyzer.ts in the ccmd repo. Scroll to line 263. The token model that ships live on ccmd.dev is six lines long:
There is no per-line gate, no scope predicate, no condition flag. The assignment is unconditional. We wrote the analyzer this way because the format works this way; the same six lines run identically against CLAUDE.md, AGENTS.md, .cursorrules, and .grokrules. If any of these formats grew a real conditional syntax, the field name would have to change. It has not.
2. What people write, vs. what fires
A typical CLAUDE.md organized by condition looks like this on the left. The system prompt the model receives looks like the right side, every turn, with all four blocks present.
The headings are labels, not predicates
# project conventions ## When working in /api - Use FastAPI route handlers, not Flask blueprints. - Validate request bodies with Pydantic v2 models from schemas/. ## When editing tests - Run pytest -x before reporting done. - Never mock the database; use the test container in docker-compose.test.yml. ## For TypeScript files only - No `any`. If you need it, use `unknown` and narrow. - Prefer `type` over `interface` unless declaration merging is needed. ## If the user mentions Stripe - Always test with the dashboard's test mode keys, never live.
- Four conditional headings make this feel scoped
- Looks like only the matching block will apply on each turn
- Reads like a switch statement to a human
The author thought they were writing four conditional rule groups. The model gets twelve plain rules with paragraph headers. On a turn where the user asks to rename a CSS class, the Stripe block and the FastAPI block both still ship. They cost roughly 60 tokens combined and they compete with the actual instruction for the model's attention.
3. The three real conditional surfaces
These are the only three surfaces the Claude Code harness enforces a condition on. None of them is inside CLAUDE.md.
| Feature | 'When X' heading inside CLAUDE.md | conditional surfaces (real) |
|---|---|---|
| Who enforces the condition | The model, by reading the heading text | The harness, by file path / skill match / tool event |
| Token cost when the condition is false | Full block fires anyway | Zero |
| Best fit for path-scoped rules ('inside /api do X') | Wrong surface | Nested CLAUDE.md at packages/api/CLAUDE.md |
| Best fit for intent-scoped rules ('when adding a route do X') | Wrong surface | Skill .md under .claude/skills/ |
| Best fit for event-scoped rules ('before any Bash, do X') | Wrong surface | Hook entry in .claude/settings.json (PreToolUse) |
| Reliability when the condition is true | Model-dependent; can be skipped under long context | Harness-enforced; runs exactly when the trigger fires |
Concrete docs: memory (CLAUDE.md and nested files), skills, and hooks. Each one runs on a different trigger; mixing them up is the single biggest source of conditional bloat we see in pasted files.
4. The five conditional shapes, and where each one belongs
Pasted CLAUDE.md files we score break down into five recurring conditional shapes. Each one has exactly one correct surface.
Path-scoped: 'when working in /api, use FastAPI'
Move it to packages/api/CLAUDE.md (or the equivalent subdirectory in your repo). That file is discovered at session start but not loaded into the system prompt until Claude reads a file under packages/api/. From that turn onward it is in context; on turns that never touch the subtree it costs zero. A heading inside the root CLAUDE.md does not buy you that.
Intent-scoped: 'when adding a new API route, do these six steps'
Move it to .claude/skills/add-api-route.md with a name and description that match the request. The skill body is not loaded into the system prompt at session start. It enters the context only when the model picks the skill, which is when the description matches the user's ask. A skill that fires once per week costs 0 tokens on the other 200 turns of the session.
Event-scoped: 'never run rm -rf without confirming'
Move it to a PreToolUse hook on Bash in .claude/settings.json. The hook is a shell command the harness runs before the tool call; it can match the proposed command, block it, and ask for confirmation. Once that hook exists, the matching sentence in CLAUDE.md should be deleted. You are paying tokens for an instruction the harness now enforces in code, not in prose.
Language-scoped: 'for TypeScript files only, no any'
Trickier. There is no first-class language predicate in Claude Code. Two acceptable answers: keep it in the root CLAUDE.md without the 'For TypeScript only' label (the model already infers language from the file extension and applies the rule selectively), or scope it via a nested CLAUDE.md at the directory where TypeScript actually lives. Do not split CLAUDE.md by language with 'For TS / For Python / For Rust' headings; you will pay the union of every language on every turn.
Identity / stack / hard prohibitions — always-on, keep in root CLAUDE.md
Lines like 'Next.js 16, Postgres on Neon, deploys to Vercel' and 'NEVER commit secrets. Why: leaked Stripe key in March 2026' belong on the always-on surface. The model needs the stack to interpret any request, and the prohibition has to be present every turn because the model has no way of predicting which turn it will apply. Keep these tight: 10 to 25 lines is plenty for most projects.
A useful pass: open your CLAUDE.md, find every heading that starts with When, If, For, or Only when, and ask which of the five shapes above each block matches. The block almost always wants to leave the file.
5. "But the model does scope by heading"
Sometimes. A capable model on a short context will treat ## When editing tests as a soft scope and skip those rules on a non-test turn. That is not a property of CLAUDE.md; it is a property of the model's reading comprehension on that particular turn. Three things degrade it: long context, an aspirational adjective inside the block ("always", "never" without an escape clause), and an absent Why: line that would have told the model when the rule actually applies.
If the rule is high-stakes (data loss, money, secrets), you do not want it on a surface where "sometimes the model gets it right" is the failure mode. Move it to a hook. The hook fires whether the model felt scoped or not.
If the rule is low-stakes (style, naming), keeping it in the root with a clear scope label is fine. Just know what you are buying: every token in the block is billed every turn, and the scope is advisory.
Want to triage your file together?
15 minutes, paste your CLAUDE.md plus any skills and hooks, leave with a shortlist of which conditional blocks belong on which surface. Free.
Frequently asked questions
Is there a way to write a conditional rule in CLAUDE.md?
No. CLAUDE.md is a plain markdown file that gets concatenated into the system prompt at session start and re-sent on every API call for the session. There is no grammar, no DSL, no front matter, no glob pattern, and no path predicate that the Claude Code harness reads from inside the file. A heading that says 'When working in /api' is text. The model reads it; the harness does not enforce it. Our analyzer codifies this at src/lib/analyzer.ts line 264: estimatedTokensFireEveryTurn = totalTokens. There is no per-line gating because per-line gating is not a property of the format.
What about the 'When X' or 'If Y' headings I see in popular CLAUDE.md examples?
They are stylistic. They make the file easier for a human reader to scan, and they nudge the model to pattern-match a rule to a context. They do not stop the file from being sent every turn, they do not reduce token cost, and they do not stop the model from applying the rule on a turn where the condition is false. If you have 200 tokens of 'when editing tests' procedure and the current turn is a CSS rename, those 200 tokens still fire and the model still has to actively ignore them.
Then how do you get conditional firing?
You move the rule to a surface that fires conditionally. Three exist. Nested CLAUDE.md (per-directory) loads only when Claude reads a file in that subtree, so it is path-scoped. Skills (markdown files under .claude/skills/) load only when the description matches the user's request, so they are intent-scoped. Hooks (entries in settings.json) fire on tool events like PreToolUse and PostToolUse, so they are event-scoped. Each one is a different shape of condition. The right one depends on what you wanted the rule to actually do.
If I write 'For TypeScript only' in a CLAUDE.md and the model is editing a Python file, does it still apply the rule?
The rule is still in the system prompt. Whether the model applies it depends on the model's reasoning, not on the file. A capable model will usually treat the heading as a scope and skip the rules under it for a Python turn. A long context plus an aspirational rule like 'never use any' will sometimes leak: the model will keep the rule active by association even when the language does not match. The cost is the same either way. The reliability is the part you cannot count on.
What's the difference between a nested CLAUDE.md and an 'if working in /api' heading?
Nested CLAUDE.md (a file at packages/api/CLAUDE.md) is enforced by the harness: it is not in the system prompt at session start, and it is only loaded into the context when Claude reads a file in packages/api/. The 'if working in /api' heading is in the system prompt from turn one and stays there for the whole session. The first one is a real conditional. The second one is a label.
Can I use a hook to make a CLAUDE.md rule fire conditionally?
You can replace the rule with a hook entirely. A hook is a shell command the harness runs at a defined event (PreToolUse on Bash, PostToolUse on Edit, UserPromptSubmit, etc.). The rule 'never run rm -rf without confirming' belongs as a PreToolUse hook on Bash that pattern-matches the command and blocks it. Once that hook exists, the matching CLAUDE.md sentence should be deleted; you are paying tokens for advice the harness now enforces in code.
Does this also apply to AGENTS.md, .cursorrules, and .grokrules?
The firing model is the same. All four formats are concatenated into the system prompt at session start and re-sent every turn. None of them has an inline conditional syntax. The sibling conditional surfaces differ per host: Codex has its own skill format, Cursor has rule files plus .cursorignore, Grok Build has tool gating. The principle holds: rules that should only apply sometimes belong on a sometimes-firing surface in whichever host you are in.
What does ccmd flag if I paste a CLAUDE.md full of 'When X' headings?
The analyzer does not flag the headings themselves; they are not wrong, they are just not load-bearing. It flags the lines inside them on the same seven checks it applies everywhere: bloat over 28 words, vague terms with no testable meaning, absolute words without an escape clause, missing 'Why' on prohibitions, duplicates, cache-busters near the top, and direct conflicts. The token math at the bottom reports the same number whether the file is organized by 'when' sections or by 'always' sections: totalTokens, every turn.
Related: do CLAUDE.md rules fire on every turn (yes, here is the proof), the four token states of a nested CLAUDE.md, or paste your file on the analyzer.
Comments (••)
Leave a comment to see what others are saying.Public and anonymous. No signup.