When each file loads
| File | Loads |
|---|---|
CLAUDE.md and CLAUDE.local.md in the folder you launch from and in every parent folder, plus ~/.claude/CLAUDE.md | At launch |
A file named in one of those with @path | At launch, with the file that names it. Relative paths start from that file, and imports can nest four deep |
.claude/rules/*.md without paths: | At launch |
CLAUDE.md in a subfolder | When Claude reads a file in that subfolder |
.claude/rules/*.md with paths: | When Claude reads a file that matches |
Every file that loads is added to the context, from the filesystem root down to your folder; none of them replaces another. After /compact, Claude Code reads the project's root CLAUDE.md again from disk, and subfolder files come back as Claude reads files there. Vendor docs
Log every load with a hook
Claude Code fires an InstructionsLoaded hook event each time one of these files loads. The event carries the file's path, its scope (User, Project, Local or Managed) and a load_reason: session_start, include, nested_traversal, path_glob_match or compact. Lazy loads also name the file that triggered them, and imports name their parent. The hook can't block or change a load. Vendor docs
Register it in the project's settings:
{
"hooks": {
"InstructionsLoaded": [
{ "hooks": [ { "type": "command",
"command": "python3 \"$CLAUDE_PROJECT_DIR/.claude/log-instructions.py\"" } ] }
]
}
}
The script writes one line per load, with paths relative to the project:
#!/usr/bin/env python3
# Append one line per instruction file Claude Code loads, with the reason.
import json, os, sys
e = json.load(sys.stdin)
root = os.environ.get("CLAUDE_PROJECT_DIR", "")
rel = lambda p: os.path.relpath(p, root) if p and p.startswith(root) else p
line = {k: rel(e.get(k)) for k in ("file_path", "trigger_file_path", "parent_file_path") if e.get(k)}
line.update(load_reason=e.get("load_reason"), memory_type=e.get("memory_type"))
with open(os.path.join(root, ".claude", "instructions.log"), "a") as f:
f.write(json.dumps(line) + "\n")
Try it
Our test project had a root CLAUDE.md that imports a review guide, a CLAUDE.md in web/, and a SQL rule that applies only to db/:
==> CLAUDE.md <==
# Shop
- Use pnpm, not npm.
- Run the tests with `pnpm test` before you say a change is done.
@docs/review.md
==> docs/review.md <==
# Code review
End every code review with a section called "Open questions".
==> web/CLAUDE.md <==
# Web
Components in web/ use the `ui-` class prefix.
==> .claude/rules/sql.md <==
---
paths:
- "db/**/*.sql"
---
Write SQL keywords in lowercase.
We ran three prompts, each in a new session, and emptied the log between them. Observed
1. A question the instructions answer
claude -p "Which package manager does this project use, and how should a code review end? Answer from your instructions only. Don't read any files."
{"file_path": "docs/review.md", "parent_file_path": "CLAUDE.md", "load_reason": "include", "memory_type": "Project"}
{"file_path": "CLAUDE.md", "load_reason": "session_start", "memory_type": "Project"}
Claude answered pnpm and an "Open questions" section. The root file and its import loaded at launch, and nothing from web/ or db/ did. The import was logged before its parent. In the third run the order was reversed, so don't read meaning into the order of lines. Observed
2. Reading a file in a subfolder
claude -p "Read web/button.html. Which class prefix should new components in that folder use, and where did that rule come from?"
{"file_path": "web/CLAUDE.md", "trigger_file_path": "web/button.html", "load_reason": "nested_traversal", "memory_type": "Project"}
web/CLAUDE.md loaded only when Claude read web/button.html, and Claude named it as the source of the ui- prefix. Observed
3. A rule scoped to a path
claude -p "Read db/orders.sql. Is its style consistent with this project's SQL rule? Say where the rule came from."
{"file_path": ".claude/rules/sql.md", "trigger_file_path": "db/orders.sql", "load_reason": "path_glob_match", "memory_type": "Project"}
The SQL rule loaded when Claude opened a file matching db/**/*.sql, and not before. Observed When a rule seems to be ignored, this log tells you whether it loaded at all, which is the first thing to rule out.
What a long file costs
Anthropic suggests keeping each CLAUDE.md under 200 lines, and Claude Code skips any file over 4 MiB. Vendor docs To put a number on it, we wrote 200 lines of made-up coding rules (20.4 KB) and sent the prompt "Reply with OK." from four otherwise identical projects. We added up the input tokens Claude Code reported, cached and uncached. Observed
| Where the 200 lines were | Input tokens | Added |
|---|---|---|
| Nowhere (a three-line CLAUDE.md) | 37,264 | |
| In the root CLAUDE.md | 42,668 | 5,404 |
In docs/big.md, imported from the root file | 42,769 | 5,505 |
In web/CLAUDE.md, and nothing in web/ was read | 37,275 | 11 |
- The 200 lines cost about 5,400 tokens on every request of every session, before you've typed anything.
- Moving text into an import tidies the file, but the imported text loads at launch too, so it costs the same.
- Text in a subfolder's CLAUDE.md, or in a rule with
paths:, costs nothing until Claude works in that folder. Instructions that only matter for one part of the code belong there.
Most of the 37,264 baseline is Claude Code's own system prompt and tool definitions, and it will differ with your tools and settings.
What we haven't verified
- The
compactreload after/compact, andCLAUDE.local.mdand user-level files, which our test project didn't have. AGENTS.mdsupport and theclaudeMdExcludessetting.- The same runs in the Claude desktop app. It starts the same Claude Code build with your user, project and local settings, as the first tutorial shows, but we ran these only in a terminal. Not verified
Last verified 2026-09-25 · review by 2026-10-25