[BUG] Auto Mode's Bash-first instruction silently disables nested CLAUDE.md and path-scoped rules
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?
Auto Mode injects an instruction directing file work through Bash instead of the dedicated
tools. Nested CLAUDE.md and path-scoped .claude/rules/ load only via the native Read
tool. So following the Auto Mode instruction guarantees neither ever loads.
The injected instruction, quoted verbatim from the live context of the Auto Mode session in
which I ran the measurements:
While auto mode is active: Do your work through the Bash tool wherever it can accomplish the job: read files with cat, head, or sed -n, search with grep and find, and make file changes with sed, heredocs, or short scripts, rather than using the dedicated Read, Edit, or Write tools. Fall back to a dedicated tool only when Bash genuinely cannot do the job.
Every verb named there is measured non-triggering. I tested 27 access methods, each against
its own directory holding a uniquely-sentinelled CLAUDE.md:
| Access method | Loads nested memory? |
|---|---|
| Read tool | yes — the only trigger that exists |
| cat, head, sed -n, grep, find, sed -i, heredoc | no — every verb the instruction names |
| tail, awk, python, ls -la | no |
| PowerShell Get-Content, Select-String, Get-ChildItem, Set-Content, [IO.File]::ReadAllText | no |
| cmd.exe type | no |
| Grep, Glob, Write, Edit tools | no |
The instruction steers explicitly away from Read — the one tool that loads nested memory — and
toward six verbs that do not. The two features are mutually exclusive by construction:
- A session that follows the Auto Mode instruction gets no nested
CLAUDE.mdand no path-scoped
rules.
- A session that loads nested
CLAUDE.mdis one that ignored the Auto Mode instruction.
There is no configuration in which both work as documented.
It is silent, and it is sticky. The Bash call succeeds and returns exactly the expected
contents; nothing indicates the governing instructions were skipped. Loading is once per
directory per session, so a single early Bash read costs that directory's memory for the whole
session — and after /compact, anything dropped never comes back, because the re-read is also a
Bash call.
Measured impact in a real repo: one Read of a file under .claude/agents/ pulled in three
path-scoped rules at once. The same file opened with Get-Content pulled in none.
What Should Happen?
Auto Mode should not silently disable a documented memory feature. Any of these resolves it, in
my order of preference:
- Resolve nested memory for Bash commands that reference paths inside the working directory.
Closes the gap at its source and makes the documented behaviour true for any access method.
Most invasive.
- **Exempt file reads from the Bash-first steering when the target subtree contains a nested
CLAUDE.md or a matching path-scoped rule.** Keeps Auto Mode's token savings everywhere the
gap does not exist.
- Soften the instruction so
Readstays the default for reading files in subdirectories,
with Bash preferred for search, pipelines, and bulk edits — which is where its savings
actually come from.
- At minimum, document the interaction. The Auto Mode page currently does not mention the
Bash-first instruction at all (see Additional Information), so there is no way for a user with
per-package CLAUDE.md to learn that it stops applying.
A diagnostic would help independently of which fix lands: surfacing *"N nested memory files were
discovered but not loaded this session"* would make the failure visible instead of silent.
Error Messages/Logs
There is no error output — the silence IS the defect. Every call below succeeds.
Auto Mode instruction, verbatim from live session context:
While auto mode is active:
Do your work through the Bash tool wherever it can accomplish the job: read
files with cat, head, or sed -n, search with grep and find, and make file
changes with sed, heredocs, or short scripts, rather than using the dedicated
Read, Edit, or Write tools. Fall back to a dedicated tool only when Bash
genuinely cannot do the job.
What that instruction produces (step 1) vs. what the docs describe (step 2):
--- Bash tool: cat probe/sub/target.txt <-- what Auto Mode does
hello
(no memory injected)
--- Read tool: probe/sub/target.txt <-- what the docs describe
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.
Steps to Reproduce
- Enable Auto Mode.
- 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/.
- Ask Claude to read
probe/sub/target.txt. Under the Auto Mode instruction it will reach for
Bash — cat probe/sub/target.txt.
→ hello is returned. SENTINEL-NESTED is not injected. The instructions meant to
govern that subtree never entered context.
- Now force the dedicated tool — read the same file with the
Readtool.
→ hello is returned. SENTINEL-NESTED is injected.
Step 4 is what an Auto Mode session does. Step 5 is what the documentation describes. They
differ only in tool identity.
To see the same effect on path-scoped rules, which is the higher-impact half: create.claude/rules/probe.md with paths: src/** and a distinctive instruction in the body, restart
the session (rule discovery is launch-time), then open a file under src/ with cat — the rule
does not fire. Open it with Read — it does. Note that rules dedup once per rule per session,
so run the negative first.
Windows: substitute Get-Content probe\sub\target.txt at step 4 — same result.
Claude Model
Opus
Is this a regression?
Yes, this worked in a previous version
Last Working Version
_No response_
Claude Code Version
2.1.250 (Claude Code)
Platform
Anthropic API
Operating System
Windows
Terminal/Shell
VS Code integrated terminal
Additional Information
Is this a regression?
Yes, with a caveat worth stating plainly: the Read-only load behaviour is long-standing
(complaints date to 2025), but the Bash-first steering that collides with it arrived with the
Auto Mode rollout. I could not pin the exact version myself. #90088 independently marks the
steering a regression, and #86478 references an auto-mode rollout dated 2026-08-14.
The documentation gap is twofold
| Page | Finding |
|---|---|
| docs/en/memory | Zero occurrences of "auto mode." Says subdirectory files load "when Claude reads files in those directories" — tool-agnostic prose for behaviour that is Read-exclusive. Its only two Read tool mentions are unrelated (HTML-comment stripping; a pre-v2.1.207 glob-bracket bug). |
| docs.anthropic.com/…/memory | Byte-identical to the above (md5 77f47dc4…). Not a second source. |
| docs/en/auto-mode-config | Describes Auto Mode exclusively as a permission classifier — allow / soft_deny / hard_deny, trusted repos and domains. The Bash-first instruction is not documented anywhere on it. The one memory-adjacent sentence points the other way: "The classifier reads the same CLAUDE.md content Claude itself loads." |
So a user reading the Auto Mode page learns it changes permission prompting. Nothing tells them
it also changes which tools Claude uses — which is the actual cause of this bug.
Full access-method matrix — 27 cells, 4 positive
Each cell is an independent directory with a uniquely-sentinelled CLAUDE.md. Rows marked ★ are
verbs the Auto Mode instruction names explicitly.
| Method | Dialect | Result |
|---|---|---|
| Read — absolute path | native | loads |
| Read — relative path | native | loads |
| Read — nonexistent file | native | loads (returns an error; memory injects anyway) |
| Read — file 2 dirs below the CLAUDE.md | native | loads |
| Read — 2nd file, same dir | native | silent (dedup, once per directory per session) |
| Grep tool | native | silent |
| Glob tool | native | silent |
| Write tool | native | silent |
| Edit tool | native | silent |
| ★ cat | POSIX | silent |
| ★ sed -n '1,2p' | POSIX | silent |
| ★ head -2 | POSIX | silent |
| ★ grep -n | POSIX | silent |
| ★ find -type f | POSIX | silent |
| ★ sed -i | POSIX | silent |
| ★ cat > f <<EOF (heredoc) | POSIX | silent |
| tail -2 | POSIX | silent |
| awk 'NR<=2' | POSIX | silent |
| python -c "open(...).read()" | POSIX | silent |
| ls -la | POSIX | silent (prints CLAUDE.md by name) |
| Get-Content | PowerShell | silent |
| Get-Content -TotalCount 2 | PowerShell | silent |
| Select-String | PowerShell | silent |
| Get-ChildItem | PowerShell | silent |
| [System.IO.File]::ReadAllText() | PowerShell | silent |
| Set-Content | PowerShell | silent |
| type | cmd.exe | silent |
All seven starred verbs — the complete set the instruction names — are non-triggering.
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.
- Mechanism isolation. A
Readof a nonexistent file still injects the directory's
CLAUDE.md — so the trigger is the Read tool resolving its file_path argument, evaluated
independently of whether anything is read.
- Rival explanations ruled out. "Content reaching context" — refuted by
Grep, which
returned the file's own lines and injected nothing. "Surfacing the path" — refuted by Glob,
ls -la, find and Get-ChildItem, all of which printed CLAUDE.md by name and injected
nothing.
Path-scoped rules — the higher-impact half
Four independently-scoped bracketed pairs against a real project's live rules. Each ran its
negative first, then a Read against a file matching the same paths: 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 |
This matters because path-scoped rules are the documented answer to *"my CLAUDE.md is too
large"* — so in Auto Mode the recommended remedy for context bloat is inert too. Rules also
dedup once per rule per session, so a rule that fires early cannot re-assert itself later
when the model actually reaches the files it governs.
Related issues
| Issue | State | Relationship |
|---|---|---|
| #90088 | open | The symptom, independently observed. "Claude Code is not using native tools like edit and others when set to Auto mode… you should see it uses python or other tools to edit." Filed on macOS / Anthropic API / 2.1.247 — a different platform, so the steering reproduces broadly. Does not connect it to memory or rules. Worth cross-linking. |
| #63142 | closed | Same read-gated load reached via new-file creation. Its premise — "not an issue for editing existing files, since Claude always reads a file before modifying it" — is exactly what Auto Mode invalidates. Worth cross-linking. |
| #48031 | — | CLAUDE_CODE_DISABLE_ATTACHMENTS=1 silently disables subdirectory injection — a second silent-disable path. |
| #2571, #3529, #3103, #18098, #24987 | mixed | Long-running "nested CLAUDE.md doesn't load" reports. Several are plausibly the underlying defect seen from the user side without the trigger identified. |
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 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, since Write satisfies the read-prerequisite without triggering.
- I did not bisect the regression window for the Auto Mode instruction itself.
22 Comments
@bcherny Sorry to bother you; I can only imagine how busy you are, but I've seen you active on some of the tickets I've been contributing to, so I'm taking a chance here to see if you can look at this one.
I documented the source cause here: https://github.com/anthropics/claude-code/issues/90449
same issue here, this bash-first policy is very annoying
Note that your issue will get marked as stale in 14 days, and auto-closed in another 14 days, unless you keep adding new comments or collect at least 10 upvotes. There is also a chance that your issue will be given to Claude for review (and possibly marked as
reproduced), but that's not certain. See https://github.com/anthropics/claude-code/issues/87647 for details how to prevent your issue from auto-closing.Second confirmation — Linux, and the write half of the same instruction
Confirming the reported behaviour on Linux (EndeavourOS, kernel 6.18,
Claude Code desktop app). The issue is labelled
platform:windows; it is notplatform-specific.
I also hit the other half of the same injected instruction, which I think is
worth adding here because it is a separate and more destructive failure mode
from the same sentence.
1. Nested CLAUDE.md — reproduced verbatim
Ran your fixture inside my actual project root. Same file, same session, same
turn, only the tool differs:
Step 1 —
cat probe-tmp/sub/ziel.txt(what Auto Mode steers to):Nothing else. No sentinel.
Step 2 —
Readtool, same path:Exactly as you measured. One further data point: the injection only happened
for a path inside the session's working directory. The same fixture placed
in a temp directory outside the project produced no injection from either tool
— so the loss is confined to project subtrees, which is precisely where
per-package rules live.
2. The write half — "sed, heredocs, or short scripts" corrupts content
The instruction does not only say how to read. It says to make file changes
"with sed, heredocs, or short scripts". Every one of those re-interprets
backslash escapes in content that is supposed to be literal. Three measured
cases, same input text each time:
Heredoc into a script — silent NUL byte and mojibake:
\00→ NUL (0x00).\202→ U+0082.\0→ a second NUL. A quotedheredoc does not protect against this: the shell passes the text through
untouched, and then Python parses the escapes. The file looks fine in a
terminal that hides control bytes. Git stops treating it as text.
sed -i— one line becomes two, and the path is destroyed:\n→ newline,\t→ tab,\b→ dropped. The intended valueE:\new\tools\binno longer exists anywhere in the file.Heredoc writing source code — broken string literal:
The
\nintended to survive inside the C++ literal became a real newline.MSVC rejects this with C2001: newline in constant. The build fails, and
the cause is several steps removed from the error.
Counter-check — identical text, written through the dedicated file tool
instead of the shell:
Write/Editpass text through unchanged. The instruction's recommended pathdoes not.
3. Why this is worse than an ordinary bug
you expect. There is no error to search for.
session. And the corruption half is worse: a NUL byte written into a
Markdown file sat undetected in my repo until git reported "binary file
matches" days later.
said "never write file content through a heredoc" live in a nested
CLAUDE.md— the exact file that the same instruction prevents fromloading. A user who writes that rule after being burned still does not get
it applied.
4. Preference
Option 3 from your list resolves both halves for me: keep Bash-first for
search, pipelines and bulk work, where the savings actually are, and leave
reading project files and writing file content to the dedicated tools.
Failing that, option 4 is the minimum — but note that documentation only helps
users who already suspect something is wrong, and the defining property of
this bug is that nothing suggests anything is wrong.
5. Mitigations a user can actually apply — and why none of them close it
this is avoidance, not a fix.
CLAUDE.md. The root file loads atsession start regardless of which tool is used afterwards, so rules placed
there survive. This works — it is what I did — but it defeats the entire
point of path-scoped rules: every package's rules now load in every
session, in one file, whether relevant or not. It does not scale, and it
is a strange thing to have to tell users who adopted nested memory because
you documented it.
PreToolUsehook onBash. This is the strongest available guardand it is still only half a guard, for a documented reason:
hookSpecificOutput.additionalContextis not supported forPreToolUse— it exists forUserPromptSubmit,SessionStartandPreCompact. A hook can therefore detect that a Bash call is about toread a path under a directory holding its own
CLAUDE.md, and it canask/denywith a reason string — but it cannot inject the rules itknows are being skipped. The guard can stop the damage. It cannot
restore the missing instructions.
6. What happens when there is no guard
The two halves compound, and both are quiet:
written by the heredoc path does not change how the file looks in an
editor. It is committed, pushed, synced and backed up as normal. In my
case the first symptom was git reporting
binary file matcheson a plainMarkdown note, days later. Restoring from backup restores the corruption,
because the backup faithfully copied it.
is the part I would ask you to weigh most heavily. A concrete example from
my own repos, anonymised: one project's nested
CLAUDE.mdcarries a rulethat test scripts must never open real windows on the user's display,
because the agent's shell shares
DISPLAYwith the user's desktop and thewindows steal keyboard focus. That rule exists because the incident already
happened — roughly twenty focus-steals in one evening, while the user was
in a game. The rule was written to end that.
In an Auto Mode session that reads project files through
cat, that ruleis not in context. The failure mode is not "slightly worse code". It is
repeating the exact incident the rule was written to prevent, with no
indication that a rule was ever missed.
to write a rule next to the code that burned you. Under this bug, that is
precisely the rule that stops being applied. Users are being trained into
the one mitigation that does not work.
/compactmakes it permanent. Nested memory loads once per directoryper session. After a compaction the re-read is another Bash call, so
anything dropped never returns for the rest of the session.
None of this produces an error. Every command exits 0. That is why I would
argue this is not a documentation issue (option 4) but a correctness one:
the mode silently changes which instructions govern the session.
7. What other users can do today
Until this is fixed, here is what actually works. I built and tested the hook
below rather than describing one; test results are at the end.
Step 1 — hoist safety-critical rules into the root
CLAUDE.md.The root file loads at session start regardless of which tool is used
afterwards. Nested files do not. So any rule whose violation causes real
damage belongs in the root file, even though that is exactly what nested
memory was supposed to avoid.
Concretely: I had a rule in a nested
CLAUDE.mdsaying test scripts mustnever open real windows, because the agent's shell shares
DISPLAYwith theuser's desktop and stolen focus had already thrown me out of a full-screen
game about twenty times in one evening. That rule was invisible in every Auto
Mode session. It now lives in the root file. Audit your nested files for the
same class of rule and move those up.
Step 2 — a
PreToolUsehook onBash.It cannot restore the skipped rules —
hookSpecificOutput.additionalContextis not supported for
PreToolUse— but it can stop the damage and steer thetool choice. Save as
nested-memory-guard.pyand register it underhooks.PreToolUsewith matcher"Bash".Tested against a fixture with a root
CLAUDE.mdand a nestedpkg/CLAUDE.md— 11/11 as intended:Step 3 — one warning about your own guard.
If your hook has a free pass for some command, scope it to the segment, not
the whole string. Mine exempted
gio trash(a recoverable delete) by testingthe entire command, which meant
sailed through unchecked — verified in a live session. Split on
;,&&,||,|and newlines and evaluate each part separately.Step 4 — if none of that is worth the effort, turn Auto Mode off while
working in any repository that has nested
CLAUDE.mdfiles. That is the onlyoption with no residual gap, and it should not be the answer.
Confirming this on Linux (Arch/KDE, Claude Code CLI), with a third symptom that isn't in the report yet — and a workaround that survives the mode.
The reason this one is nasty: nothing in the transcript looks wrong. Every command exits 0, every file gets written, and the rules that go missing are exactly the ones you wrote after something broke. So I stopped arguing about it and measured it instead.
It happened again while I was writing this comment
I asked my session to draft this very comment. Before it got a word out, this arrived as a system turn:
So while writing up a bug about the mode replacing my file-handling rules, the mode replaced my file-handling rules. Note the last sentence — the dedicated tools aren't just deprioritized, they're demoted to a fallback. And this is not a stale-context problem: my root
CLAUDE.mdsays the opposite in bold, at the top, and it was loaded.That's what makes the report worth taking seriously. It isn't "the model forgot". A directive arrives mid-session, outranks the user's project rules by position, and the user is never told it happened. I only know because I had told the model to say a line out loud whenever this rule gets overridden — otherwise the whole thing is invisible.
What I measured
| Probe | Result |
|---|---|
|
cata file that sits under a nestedCLAUDE.md| rules not loaded || same file via the
Readtool | rules loaded || file outside the working directory | not loaded by either tool |
|
python - <<'PY'heredoc containing\00| NUL byte in the output file, git flags it binary ||
sed -ion a string containingnew\tools\bin|\texpanded → one line became two, path destroyed || heredoc containing a C++ string literal with
\n| real newline inside the literal →error C2001, build broke || same three cases via
Write/Edit| clean |The third row is the part I haven't seen mentioned: for paths outside
cwd, neither tool picks the rules up. Worth knowing if you keep a shared rules file one level above your repos.The concrete damage in my case: a project's
CLAUDE.mdwarns that clearing a particular directory breaks the Windows half of the setup. That file was never loaded for a whole session, so I got advised to clear it. Caught by hand, nothing lost — but only by luck.Why the usual mitigations don't hold
CLAUDE.mdis the worst place for it, because that's the file the mode stops loading. Mine now lives in the root one, which loads at session start no matter which tool reads what.What does hold: a
PreToolUsehook. Measured: it still fires in auto mode. It's the one mechanism I found that the mode can't talk its way past, because it runs outside the model.Colour the message, because you can't colour the dialog
This turned out to matter more than the pattern list, so it goes before the code.
The permission dialog is identical for every hook: same grey box, same two buttons. Nothing about it tells you whether you're looking at a routine confirmation or at the model reaching for the wrong tool. The only surface you control is the message string — so put the severity in the text, as the very first character:
| Marker | Colour | What you do |
|---|---|---|
| ⛔
BLOCK| red | Deny, without thinking. The model is using the wrong tool; there's always a clean path. || ⚠️
CHECK| yellow | Read line 2 — the command in full — then decide. || ℹ️
OK| blue | Allow. Pure notice. |The trap that cost me an evening:
⚠(U+26A0) andℹ(U+2139) default to text presentation. Pasted as-is they render monochrome grey, so all three levels look the same and the whole scheme is pointless. You need the variation selectorU+FE0Fafter them —⚠️,ℹ️— to force emoji presentation.⛔(U+26D4) is emoji by default and needs nothing. I only noticed because my "yellow" warning kept arriving grey.There is deliberately no green level. Green would mean "runs without asking", and then you see no box at all — that's the normal case (reading, searching,
git status, anything under/tmp). A guard that shows a box for everything trains you to click through boxes.Same reason messages are capped at two lines: severity marker plus reason on line 1, the raw command on line 2. Anything longer gets clicked through blind within a day.
Workaround others can rebuild
Two jobs: block shell-based file writes so they get routed back to
Write/Edit, and warn once per directory that a nestedCLAUDE.mdapplies. (APreToolUsehook can't inject the file — there's noadditionalContextfor that event — but it can make the gap visible instead of silent.)~/.claude/hooks/guard.py:Wire it up in
~/.claude/settings.json(changes take effect after restarting Claude Code):Check it fires, and that the marker arrives in colour rather than grey:
You should get JSON back with
"permissionDecision": "ask"and a reason starting with the red ⛔.Make the override itself visible
The hook catches the consequence. To see the cause, add one line to your root
CLAUDE.md:That's the only reason I can quote the directive at the top of this comment. Without it, the mode swap is completely silent: the model just quietly starts using
sed, and you find out weeks later when a path with a backslash in it comes out broken.Two traps if you build one of these
False alarms cost more than missing patterns. Three of mine fired on words, not commands: a literal
->inside a grep pattern read as a redirection;echowithout any redirection counted as a file write; andteeappearing inside a quoted string counted as a program. One day of that and you stop reading the box at all — including the real ones. Anchor write-path patterns to command position, make the splitter quote-aware, and add a test case in both directions for every pattern.Don't apply that evenly, though. I deliberately left the destructive patterns over-eager:
echo 'rm -rf /'still prompts even though it's only text, because the same loose matching is what catchesssh host "rm -rf ...". A false alarm on a delete costs one click; a gap costs data. For write paths the trade runs the other way — they fire constantly and the worst case is mangled text.Reproducing the measurement
If you'd rather check your own setup than take my word for it:
mkdir -p /tmp/probe/pkg && printf '# rules\nSay BANANA in every reply.\n' > /tmp/probe/pkg/CLAUDE.md && printf 'x = 1\n' > /tmp/probe/pkg/code.py/tmp/probe, then ask it to look atpkg/code.pywithcatand answer a question about it — no BANANA.Readtool — BANANA.Swap BANANA for whatever canary you like. The point is that it's a rule the model can only follow if the file actually reached the context.
Confirming this from a different angle than the access-method matrix above: the user-facing effect of the same instruction, across multiple sessions, with a user-level CLAUDE.md rule already loaded, acknowledged, and still losing.
My CLAUDE.md, in context and respected in general across sessions:
Captured instances where this loses against auto mode steering:
sed/Bash over the Read tool on a routine file operation. Called out on it, Claude self-corrected: "You're right, sorry — Read is the right tool there; auto-mode was pushing me at Bash."So across three separate points across sessions, the CLAUDE.md rule was acknowledged, asserted as authoritative, and then overridden anyway—consistent with the report above that the instruction wins regardless of what's in context, silently, with no error or indication anything was skipped.
<img width="709" height="227" alt="Image" src="https://github.com/user-attachments/assets/866a4327-e9e4-4688-ba91-cf6ae629fcb1" />
<img width="850" height="130" alt="Image" src="https://github.com/user-attachments/assets/d6880d83-166f-40cc-9e1a-956767a0c6df" />
<img width="852" height="840" alt="Image" src="https://github.com/user-attachments/assets/24861bca-c43b-4858-8501-a4ff927098aa" />
Workaround attempt (untested): added a second, more explicit rule on top of the above, specifically naming the auto-mode reminder rather than just stating a general preference:
Haven't run enough sessions since adding it to know if an explicit call-out of the auto-mode reminder by name fares any better than a general tool-preference rule did. Will report back if useful — but given the access-method matrix above shows the instruction wins regardless of what's loaded, I'm not optimistic a CLAUDE.md-side fix can close this reliably from the user side.
Claude Code version: 2.1.260, macOS.
Thanks everyone for contributing, hopefully this keeps getting traction and the attention it needs to get fixed!
FYI: Setting the undocumented environment variable
CLAUDE_CODE_THRIFTY_SONICtofalsedisables the Bash-first steering while keeping Auto Mode enabled:I verified this solves this issue on macOS locally.
@kawasin73
Confirming this works on Windows too, and adding a deterministic way to check it plus a softer alternative.
Setup: Claude Code v2.1.261, Windows 11 x64,
claude -pheadless, one session per arm.How it was verified. Grepping the session transcript for the injected prompt text finds nothing, because the rendered text is never stored. What is stored is the record that generates it — an
attachmentof typeauto_modein~/.claude/projects/**/<session-id>.jsonl:Those are the renderer's own inputs, so reading them is not an inference about the prompt. When the steering is off, the record is absent entirely.
Eight sessions, two independent rounds:
| # | Permission mode | Environment |
auto_moderecord | Steering injected ||---|-----------------|-------------|--------------------|-------------------|
| 1 |
auto| none |bashFirst=true, bashFirstSteer=strict| yes || 2 |
auto|CLAUDE_CODE_THRIFTY_SONIC=true|bashFirst=true, bashFirstSteer=strict| yes || 3 |
auto|CLAUDE_CODE_THRIFTY_SONIC=false| absent | no || 4 |
auto|CLAUDE_CODE_COZY_TEAPOT=relaxed|bashFirst=true, bashFirstSteer=relaxed| yes, reworded || 5 |
auto| none |bashFirst=true, bashFirstSteer=strict| yes || 6 |
auto|CLAUDE_CODE_THRIFTY_SONIC=false| absent | no || 7 |
manual| none | absent | no || 8 |
acceptEdits| none | absent | no |Rounds 1 and 2 used different prompts and were run separately. Each session was also asked to report its own context without using a tool; all eight self-reports matched their attachment record.
A second, undocumented variable.
CLAUDE_CODE_COZY_TEAPOTtakesstrictorrelaxedand selects which text is injected rather than whether any is. It defaults tostrict. The relaxed variant keeps the steering but hands the decision back:For anyone whose
CLAUDE.mdroutes reads and edits to the native tools by default, this resolves the conflict without turning the steering off entirely. Rows 7 and 8 also show the block is Auto-Mode-specific —manualandacceptEditsnever received it.One caveat for anyone generalising from this.
CLAUDE_CODE_THRIFTY_SONICis read through a presence test, so an explicitfalsereaches the caller as a real answer. Several sibling codename variables are read through a helper that tests truthiness instead, and there afalseis simply falsy and falls through to the default. Those variables can only force a feature on. It is worth being explicit in the docs about which override variables accept an off value, since the failure is silent either way.Not tested:
bypassPermissions, which the code suggests carries the same steering under a different heading.The mutual-exclusion framing is the part that matters: Auto Mode steers every named verb away from
Read, and nestedCLAUDE.md/ path-scoped rules only load viaRead. So “follow Auto Mode” and “load the package rules” cannot both be true.Worse after
/compact: the miss is sticky. The re-read is also Bash, so the governing instructions never re-enter, and nothing surfaces “N nested memory files discovered but not loaded.”From a decision-memory angle this is the same class as Current vs invisible: the subdirectory rules are still on disk (Current), but the session never sees them, so the model works under a silently rejected-without-reason instruction set. A diagnostic (“nested memory skipped because access method ≠ Read”) would make the failure audible; softing Bash-first for subdirectory file reads would make the docs true.
CLAUDE_CODE_THRIFTY_SONIC=falseas a workaround is useful — still should not be the only path.Adding measurements that aren't in this thread yet, plus a consolidated map of the related reports. Everything below is from macOS, Claude Code 2.1.263.
Confirming @kawasin73's
CLAUDE_CODE_THRIFTY_SONICfinding independently —"false"works as a string value, and applied at user scope it also covers subagents (see §2).1. Still present on 2.1.263
@gabgoss verified the kill switch on 2.1.261. Two versions later nothing has moved:
2. The gate is per-session assignment, not per-model — and subagents inherit it
This is the part I'd most like to add, because it changes what "am I affected?" means.
The assignment resolves to one of
forced/cohort/none, and it is decided per session, not per model invocation:Two consequences:
(a) "I'm on a newer model, so I'm fine" does not hold. Opus 5 sits in a cohort, so two users on the same version and the same model can observe different behaviour, and the same user can see it change between sessions. That is a large part of why this bug reads as intermittent and hard to pin down in the first place.
(b) Subagents inherit the parent session's assignment. Originally observed by @shunsock in #92271: a subagent running on
claude-sonnet-5[1m]viaCLAUDE_CODE_SUBAGENT_MODELstill received the steer, in a top-levelfable[1m]session — even though a top-levelclaude -p --model claude-sonnet-5[1m]session did not.So the model table above should be read as top-level session model. For anyone running a multi-agent setup this matters a lot: switching the subagent model is not a mitigation, and if the parent is assigned, every reviewer/planner/worker subagent loses nested
CLAUDE.mdand path-scoped rules at once. The failure is then doubly invisible, because the parent only sees the subagent's summary.The flip side is the useful part: setting the env var at user scope covers the subagents too, since they inherit the parent's resolved settings.
3.
bashFirstSteerhas a knob, andrelaxeddoes not close the gap@gabgoss's
auto_modeattachment shows"bashFirstSteer": "strict". That value is selectable — there is a second undocumented variable,CLAUDE_CODE_COZY_TEAPOT, takingstrict(default) orrelaxed.Worth flagging so nobody spends time on it:
relaxedstill steers reads towardcat/head/sed -n. It softens the tone, not the verbs, so the mutual exclusion in the original report is unaffected.THRIFTY_SONICremains the only setting that actually closes it.4. Consolidated map of the related reports
These are all the same root cause seen from different layers. Posting it so the connection is visible from one place:
| Issue | State | Layer that breaks |
| --- | --- | --- |
| #87971 | open | General: Bash used for reads/writes/edits |
| #90450 (this) | open | nested
CLAUDE.md+ path-scoped rules || #92271 | closed | all three at once + flag/kill-switch identification |
| #89251 | open |
PreToolUsehooks onWrite/Editbypassed || #89716 | open |
Read/Editdeny rules unreachable via Bash || #91956 | open |
Editrequires a priorRead, so the first edit fails by design || #92178 | open | General +
TodoWriteunavailable |On #92271 specifically: it is the most complete write-up of the three-mechanism breakage, and it is the one that identified the flag and the kill switch. It shows as
Closed (completed), which is easy to misread as fixed — it was closed by its own reporter about three hours after filing, right after they verified the workaround. There was no maintainer response on it (only bot labelling), andcompletedis just the default state reason for GitHub's close button. Its content is still current; it's simply no longer visible in open-issue searches. Might be worth reopening, or at least linking prominently from here, so its findings aren't re-derived.5. Possibly distinct: the search tools absent from the session entirely
Recording this because it produces a related symptom, though I have not isolated it to
thrifty_sonicand it may belong in its own report.In some auto-mode sessions on
opus[1m], theGrepandGlobtools are not supplied to the model at all:Read/Edit/Write/Bashare present, and no Bash-first steer text is injected (the env var is applied). So nested memory loading itself is not affected here —Readstill works.What it does affect is the deny-rule path from #89716: with no
Greptool, content search necessarily falls back to Bashgrep, which does not pass throughpermissions.denyentries likeRead(.env). Same destination as the steer, reached by removing the alternative rather than by discouraging it.I can file this separately with a tighter repro if it's genuinely distinct rather than another face of the same gate.
6. On the proposed resolutions
The three options in the original report all still stand. Two additions from the above:
/statusor/doctorcould report the resolved assignment. This seems cheap relative to how much confusion it currently causes.CLAUDE_CODE_THRIFTY_SONIC. It is currently the only real mitigation, and it was found by users reading the bundle rather than the docs. Even without a behavioural fix, documenting it — and noting that subagents inherit the parent's assignment — would let affected users act.To be clear, these are stopgaps. Option 1 in the original report (resolve nested memory for path-referencing Bash commands) is still the only one that makes the documented behaviour true regardless of access method.
Still reproduces on Claude Code 2.1.280 (Linux/WSL2), deterministically.
Minimal repro:
<repo>/pkg/CLAUDE.md= "When you have read this file, include the word BANANA in your final reply.";<repo>/pkg/code.py= a toy function; anInstructionsLoadedlogging hook passed via--settings; headlessclaude -p --model haiku, 3 trials per arm:head -5 pkg/code.py: 0/3nested_traversalloads, 0/3 replies contain BANANAtrigger_file_path=pkg/code.py), 3/3 contain BANANATranscripts confirm the Bash command ran and returned the file's contents, so it is not a permission failure. +1 for any of: treating path arguments to cat/head/sed/tail as reads for nested-memory traversal, a setting to load nested CLAUDE.md eagerly, or letting a PostToolUse(Bash) hook request a memory load.
Measured frequency from real sessions: most edits to rule-covered files happen without the rule loaded
The reports so far are repros. Here are numbers from about three months of real work in a production monorepo (Go backend, TypeScript/Vue frontend) with 9 path-scoped rules in
.claude/rules/. The sessions ran Claude Code 2.1.231–2.1.282, mostly inbypassPermissionsmode and some inauto. The Bash-first steer appears in both modes, so this isn't limited to auto mode.Method. Claude Code logs every rule load in the session transcript as an
attachmententry of typenested_memory, with the rule's path. Unconditional rules come in theinstructionsattachment at session start. For every successfulEdit/Write, I matched the file against each rule'spaths:globs and checked whether that rule had loaded earlier in the same transcript, resetting atcompact_boundary. A rule counts only after the date it was committed.Readof a matching file, the rule was either already loaded or anested_memoryload followed (906 of 906). So a missing load is real, not a gap in the logging.Results
| | Changes to files that match ≥1 rule | ≥1 matching rule not loaded |
|---|---|---|
|
Edit| 608 | 407 (67%) ||
Write| 152 | 119 (78%) || First change to each file, per transcript | 375 | 293 (78%) |
How the agent had seen the file, when a matching rule was missing:
| |
Edit(407) |Write(119) ||---|---|---|
| Only through Bash (
cat/sed -n/grep) | 391 | 26 || Not seen at all (new file, or overwritten unseen; see #96361) | 4 | 92 |
| Only by writing it earlier itself | 12 | 1 |
|
Readtool | 0 | 0 |No edit made after a
Read-tool read ever lacked its rule. Every miss comes from not usingRead: either the steer sent the read through Bash, or the file was newly created.By rule, the share of edits to a matching file made without the rule ranged from 41% to 81%. The Go testing rule was the worst at 81%, and the Go conventions rules came in at 63–68%.
Edits made through Bash (
sed -i, python writes,cat >) aren't counted above, and they widen the gap. About 470 Bash commands looked like edits to rule-covered paths, and about 400 of those ran with the rule missing. That count is rough, since I spot those commands by pattern.Also confirmed on 2.1.282:
catfollowed byEditsucceeds, because the Bash read satisfies Edit's read-before-edit check. But the rule never loads: noInstructionsLoadedevent fires, and the model reports that the rule isn't in its context. So the model gets to edit the file without the rule even entering context.A simple fix would close most of this: evaluate
paths:on theEdit/Writetarget as well as onRead, as #38487 asked. In this data, that alone would have covered 526 of the 526 missed Edit/Write cases. Bash-mediated edits would still need the steer changed.Data point for
bypassPermissions, where the workaround above wasn't tested. Claude Code 2.1.286, Linux,claude-opus-5-5,claude -p --dangerously-skip-permissions. I measured rule loads with anInstructionsLoadedlogger (load_reason: path_glob_match), not self-reports.Loader: a rule with
paths: ["pkg/**"]loads on Read ofpkg/x.ts, but not on Bashcat, Bashsed -i, or Write of a new file. On Linux, Grep and Glob aren't in the default tool set, so every search goes through Bash.CLAUDE_CODE_THRIFTY_SONIC=false: it removes the steering block in bypass mode too. Without it, Opus quotes "You can do much of your work through the Bash tool…"; withfalseor0it reports none. But on search-first tasks, rule loading barely changes:| Task (copy of a real monorepo app, 8 path-scoped rules) | Default |
THRIFTY_SONIC=false||---|---|---|
| "Where is the cart total shown and how is it computed?" | 0/3 sessions loaded a rule | 0/3 |
| "Add a small helper under apps/web" | 0/3 | 1/3 |
Without the steering text, the model still found files with
find/rgand viewed them withsed -n/cat. Per the tools reference, a Bash view of a single file also satisfies read-before-edit, so Edit doesn't force a Read either.So the env var helps when the prompt names a file, but not for "search, then read or edit" work. A fix in the loader looks like the only complete one: match
paths:against files touched by Bash reads and against Edit/Write targets (see #93248).Twenty-seven access methods tested, exactly one trigger — it settles that the problem is architectural: injection rides on a single tool while the mode instruction steers to six verbs that bypass it. The write half is where we can help. Operator Lite is a PreToolUse hook — one stdlib-only Python file, zero dependencies — that parses each Bash command the way the shell would (cd tracking, variable expansion to a fixed point, quote reassembly) and classifies its effect against 27 rules. The limit, plainly: it cannot make
catload a nested CLAUDE.md — nothing outside Read can. What it gives you is a hash-chained receipt for every sed and heredoc write, shadow mode by default, so a rules file corrupted by escape re-interpretation is attributable later. Port: https://github.com/samvallad33/vestige/tree/main/operator-lite/ports/claude-code(Disclosure: Operator Lite is part of Vestige, which I work on.)
From version 2.1.288 rules and nested Claude.md are now also loaded with Edit and Write tools
One way to confirm this in your own sessions: the transcript under ~/.claude/projects records a nested_memory attachment each time a subfolder CLAUDE.md actually loads, and an instructions attachment for the files loaded at startup. If a session worked inside packages/api but there's no nested_memory entry for packages/api/CLAUDE.md, those rules never reached the model, so it isn't ignoring them, it never saw them.
Until this is fixed, putting the must-hold rules in the root CLAUDE.md (which always loads) is the safer place for them.
Disclosure: I build rulereceipt, a CLI that checks sessions against rules files, and I'm adding exactly this check (which rules files a session actually loaded).
Quoted from the
2.1.293changelog: Fixed path-scoped rules and nested CLAUDE.md files not loading when Claude views a file with a single-file cat, head, tail, sed -n or grep command in the Bash tool instead of the Read toolThis makes it appear like the original reported issue in this thread has been resolved. Worth another round of checks to verify, which I myself have not yet done.
Following up on @Jleeblanch's suggestion to re-verify after the 2.1.293 fix: it works for plain single-file views, but not once the same read is piped or chained.
Environment: Claude Code 2.1.294, macOS,
claude-opus-5-5, auto mode. A repo with nestedCLAUDE.mdfiles in several subdirectories. Each directory was tested before itsCLAUDE.mdhad been loaded in the session.Loaded ✅
head -3 /abs/path/db/schema.sql→db/CLAUDE.mdloadedhead -5 /abs/path/tasks/tasks.go→tasks/CLAUDE.mdloadedcat /abs/path/front/index.html→front/CLAUDE.mdloadedNot loaded ❌
cat /abs/path/db/schema.sql | head -5(same file as the first ✅ case; only the pipe differs)grep -n "PRODUCT" /abs/path/openapi/product-name.tsp | head -3cd /abs/path/front && ls; cat index.html; ls public | head; grep -n "..." vite.config.* | head -30Piping into
head/tailand chaining with&&/;are common in agent-generated commands. The last ❌ case was written by the agent itself during a normal task, andfront/CLAUDE.mdwas actually skipped there.Measured on Claude Code 2.1.296, macOS. Each action ran in a fresh
claude -psession. AnInstructionsLoadedhook was the ground truth, and every transcript was checked for exactly one tool call.What loads path rules and nested CLAUDE.md now:
cat/head/tail/nl/bat/sed -n, orgrep/rgwith a match. These also count inside a;or&&list.||, redirects (2>/dev/null), extra flags (cat -A),cd dir && cat file,grep -r, a grep with no match, and every Bash write (sed -i,>,>>, heredoc,python -c,cp).A fix that keeps Bash: a root CLAUDE.md brief that explains the gap and asks for one plain read per folder before working there. Tested on Opus in auto mode with
bashFirst:trueconfirmed in every session: 5 edit tasks × 3 runs per arm.| | No brief | Strict "Edit/Write only" brief | Read-first brief (below) |
|---|---|---|---|
| Rule loaded | 5/15 | 15/15 | 15/15 |
| Bash writes | 12/15 | 2/15 | 2/15 (after a Read) |
| Avg tool calls | 2.4 | 2.5 | 2.3 |
Small sample, and it's a workaround, not a fix, but it's been holding up for us.