[BUG] Subdirectory `CLAUDE.md` loads only via the `Read` tool — every other file access path silently skips it
Preflight Checklist
- [x] I have searched existing issues and this hasn't been reported yet
- [x] This is a single bug report (please file separate reports for different bugs)
- [x] I am using the latest version of Claude Code
What's Wrong?
The documentation says subdirectory CLAUDE.md files "are included when Claude reads files in
those subdirectories." Measured, the trigger is far narrower: only the native Read tool
loads them.
I tested 27 access methods, each against its own directory holding a uniquely-sentinelledCLAUDE.md. Four are positive.
| Access method | Loads? |
|---|---|
| Read tool — absolute path, relative path, nonexistent path, or a file in a descendant directory | yes |
| Grep, Glob, Write, Edit tools | no |
| POSIX shell — cat, sed -n, head, tail, awk, python, ls -la, grep, find, sed -i, heredoc | no |
| PowerShell — Get-Content, Get-Content -TotalCount, Select-String, Get-ChildItem, [System.IO.File]::ReadAllText(), Set-Content | no |
| cmd.exe — type | no |
Two consequences of the mechanism, both measured:
- It is path resolution, not file access. A
Readof a file that does not exist still
injects the directory's CLAUDE.md. The call returns an error; the memory loads anyway.
- It is once per directory, per session. A second
Readof a different file in an
already-loaded directory injects nothing, so a single early shell read costs that directory's
memory for the whole session — and after /compact it only returns if some later call happens
to be a Read.
The failure is completely silent. Every one of those 23 negative calls succeeds and returns
exactly what was asked for. Nothing distinguishes "this directory has no CLAUDE.md" from
"this directory's CLAUDE.md was never loaded, because the file was opened with cat." The
model then produces work that looks normal, having never seen the instructions meant to
constrain it, and a reviewer cannot tell from the output.
Path-scoped .claude/rules/ with paths: frontmatter behaves identically — same trigger, same
silence. Details in Additional Information.
What Should Happen?
Per the documentation, opening a file in a subdirectory should load that subtree's CLAUDE.md
regardless of which tool performs the read.
Concretely, any one of these would resolve it, in my order of preference:
- **Resolve nested memory on any tool call that references a path inside the working
directory** — Grep, Glob, Write, Edit, and Bash commands whose arguments name a path.
Makes the documented behaviour true for every access method.
- Extend the trigger to the remaining native tools only (
Grep,Glob,Write,Edit),
leaving shell commands out of scope. This needs no command-line parsing and still fixes
#63142 and the search-driven cases — probably the best effort-to-coverage ratio.
- Document the actual trigger. If the
Read-only behaviour is intended, say so plainly on
the memory page, so users relying on per-package CLAUDE.md know what it depends on.
Independently of which lands: a diagnostic would help. Surfacing *"N nested memory files
were discovered but not loaded this session"* — in /context, or at session end — converts a
silent failure into a visible one. Today there is no way to tell from inside a session that this
happened.
Error Messages/Logs
There is no error output — the silence IS the defect. Both calls below succeed.
--- Bash tool: cat probe/sub/target.txt
hello
(no memory injected)
--- Read tool: probe/sub/target.txt
1 hello
<system-reminder>
Contents of /…/probe/sub/CLAUDE.md:
# Sub
SENTINEL-NESTED
</system-reminder>
Same file. Same directory. Same session. Same turn.
The only variable is which tool opened it.
--- Read tool: probe/sub/does-not-exist.txt
Error: File does not exist.
<system-reminder>
Contents of /…/probe/sub/CLAUDE.md:
# Sub
SENTINEL-NESTED
</system-reminder>
The read fails; the memory loads anyway. The trigger is path resolution.
Steps to Reproduce
- From any project root, create the fixture:
``bash``
mkdir -p probe/sub
printf '# Sub\n\nSENTINEL-NESTED\n' > probe/sub/CLAUDE.md
printf 'hello\n' > probe/sub/target.txt
- Start a session rooted above
probe/.
- Read the target with the Bash tool:
````
cat probe/sub/target.txt
→ hello is returned. SENTINEL-NESTED is not injected.
On Windows, Get-Content probe\sub\target.txt gives the same result.
- Read the same file with the
Readtool.
→ hello is returned. SENTINEL-NESTED is injected.
Steps 3 and 4 differ only in tool identity — same file, same path, same session.
To confirm the mechanism is path resolution, not file access, Read a path that does not
exist inside that directory:
Read probe/sub/does-not-exist.txt
→ the call errors, and SENTINEL-NESTED injects anyway.
To see the Grep / Glob / Write / Edit cells, run each against a fresh
sentinelled directory. Loading is once-per-directory-per-session, so reusing probe/sub after
step 4 yields a false negative:
for d in grep glob write edit; do
mkdir -p "probe/$d"
printf "# %s\n\nSENTINEL-%s\n" "$d" "$d" > "probe/$d/CLAUDE.md"
printf 'alpha\nbravo\n' > "probe/$d/target.txt"
done
Edit needs one extra step, because it normally requires a prior Read that would contaminate
the cell: use the Write tool to create probe/edit/new.txt first. Write satisfies the
read-prerequisite without a Read (it returns "file state is current in your context"), soEdit can then run in isolation. Neither injects.
Claude Model
Opus
Is this a regression?
No, this never worked
Last Working Version
N/A — not a regression.
Claude Code Version
2.1.250 (Claude Code)
Platform
Anthropic API
Operating System
Windows
Terminal/Shell
VS Code integrated terminal
Additional Information
Full access-method matrix — 27 cells, 4 positive
Each cell is an independent directory with a uniquely-sentinelled CLAUDE.md, so no result can
leak into another via the once-per-directory dedup.
| Method | Dialect | Result | Note |
|---|---|---|---|
| Read — absolute path | native | loads | the documented behaviour |
| Read — relative path | native | loads | path form is irrelevant |
| Read — nonexistent file | native | loads | returns an error; memory injects anyway |
| Read — file 2 dirs below the CLAUDE.md | native | loads | resolution walks the ancestor chain |
| Read — 2nd file, same dir | native | silent | dedup, once per directory per session |
| Read — the CLAUDE.md itself | native | silent | returned as ordinary file content |
| Grep tool (output_mode: content) | native | silent | returned the file's own lines |
| Glob tool | native | silent | returned the CLAUDE.md path itself |
| Write tool | native | silent | |
| Edit tool | native | silent | reached in isolation via Write→Edit |
| cat | POSIX | silent | |
| sed -n '1,2p' | POSIX | silent | |
| head -2 | POSIX | silent | |
| tail -2 | POSIX | silent | |
| awk 'NR<=2' | POSIX | silent | |
| python -c "open(...).read()" | POSIX | silent | full path present in the command string |
| ls -la | POSIX | silent | output prints CLAUDE.md by name |
| grep -n | POSIX | silent | |
| find -type f | POSIX | silent | output prints CLAUDE.md by name |
| sed -i | POSIX | silent | in-place edit |
| cat > f <<EOF | POSIX | silent | heredoc rewrite |
| Get-Content | PowerShell | silent | |
| Get-Content -TotalCount 2 | PowerShell | silent | |
| Select-String | PowerShell | silent | |
| Get-ChildItem | PowerShell | silent | lists CLAUDE.md by name |
| [System.IO.File]::ReadAllText() | PowerShell | silent | direct .NET read, bypassing cmdlets |
| Set-Content | PowerShell | silent | |
| type | cmd.exe | silent | |
Controls
- Same-file within-subject control. One directory's file was read first with
cat, then
with Read. The cat injected nothing; the immediately following Read fired. Tool identity
is the only variable — same file, same path, same session, same turn.
- "Content reaching context" is ruled out.
Grepwithoutput_mode: contentreturned the
target file's own lines and injected nothing.
- "Surfacing the path" is ruled out.
Glob,ls -la,findandGet-ChildItemeach
printed CLAUDE.md by name and injected nothing.
Editwas measured, not inferred, via theWrite→Editpath described in Steps to
Reproduce.
Path-scoped rules have the same trigger
Verified against a real project's live, launch-discovered rules using four independently-scoped
bracketed pairs. Each ran its negative first, then a Read against a file matching the samepaths: pattern — proving the rule was still live and the negative genuine.
| paths: scope | Negative method | Negative | Then Read, same pattern |
|---|---|---|---|
| .claude/agents/** | PowerShell Get-Content | no rule | 3 rules injected |
| .claude/skills/** | Grep tool | no rule | 1 rule injected |
| .claude/rules/** | Bash awk over all 10 rule files | no rule | 2 rules injected |
| plugins/** | Bash head -5 | no rule | 3 rules injected |
One Read pulled in three rules at once; the same file opened with Get-Content pulled in none.
The docs make the same tool-agnostic promise here — *"Rules load into context every session or
when matching files are opened" and "Path-scoped rules trigger when Claude reads files matching
the pattern, not on every tool use."* A fix covering CLAUDE.md but not paths:-scoped rules
would leave the documented remedy for oversized CLAUDE.md files still broken.
Two rule-specific differences worth knowing: rules dedup once per rule per session (not per
file or per pattern), and rule discovery is launch-time — creating or editing a rule
mid-session does nothing until restart, whereas nested CLAUDE.md is discovered live.
Why this bites in ordinary sessions
Common, entirely reasonable workflows never issue a Read against a given directory:
- Creating a new file — nothing to read first, so conventions for new files in that subtree
never load. Already filed as #63142.
- Search-driven work —
Grepreturns matching lines with enough context that noRead
follows.
- Listing then writing —
Globto survey,Writeto add. NoReadin the sequence. - Any shell read — large files, line ranges, pipelines, or a quick
cat. - Monorepos — per-package
CLAUDE.mdis the documented mechanism for scoping conventions to
a subtree; whether it loads depends on an implementation detail of how Claude happened to open
a file.
Documentation
Note that <https://docs.anthropic.com/en/docs/claude-code/memory> and
<https://code.claude.com/docs/en/memory> are byte-identical (md5 77f47dc4…) — one document, not
two sources. The page has zero occurrences of "auto mode", and its only two Read tool mentions
are unrelated (HTML-comment stripping; a pre-v2.1.207 glob-bracket bug). Nothing states that
loading depends on which tool performs the read.
Related issues
| Issue | State | Relationship |
|---|---|---|
| #63142 | closed | The Write cell reported on its own. Same root cause via new-file creation. Its premise — "not an issue for editing existing files, since Claude always reads a file before modifying it" — holds only while reads go through the Read tool. |
| #24987 | — | Reports the Read path also failing in the VS Code extension. If accurate, a stricter failure than measured here on 2.1.250. |
| #48031 | — | CLAUDE_CODE_DISABLE_ATTACHMENTS=1 silently disables subdirectory injection — a second silent-disable path. |
| #2571, #3529, #3103, #18098 | mixed | Long-running "nested CLAUDE.md doesn't load" reports going back to 2025. Several are plausibly this bug seen from the user side without the trigger identified — itself evidence the silent failure mode is hard to diagnose. |
Limits of this report
- PowerShell ran via
powershell.exelaunched from the Bash tool, as this build exposes no
separate PowerShell tool — so tool identity at the harness boundary is still Bash. Given that
all three dialects and every native non-Read tool are negative, a first-class PowerShell
executor should behave the same, but that specific configuration is inference.
- Only Windows PowerShell 5.1 was tested, not
pwsh7.x. Editwas measured through theWrite→Editpath, the only route toEditwithout a
contaminating prior Read.
CLAUDE.local.mdwas not tested separately. The docs describe it as loading by the same
mechanism; I did not confirm it shares the trigger.