[Bug] Auto-mode "bashFirst" system prompt instructs sed/heredoc file edits instead of Edit/Write tools

Status Open
Reported on v2.1.234
Maintainer reply None cached
Activity 19 comments · opened Aug 19, 2026

Bug Description
Auto-mode tries to edit files via Python scripts thanks to this bad instruction, please fix it:

It's hardcoded in the CLI binary — not your config. I found it in /opt/claude-code/bin/claude (the real binary; /usr/bin/claude is a 4-line wrapper). The raw template: …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 ${Qs}, ${xl}, or ${fu} tools. Fall back to a dedicated tool only when ${_i} genuinely cannot do the job. and the gate that selects it: s = tools.has(Bash) && (tools.has(Edit) || tools.has(Write)) && d5o(); return [{ type:"auto_mode", bashFirst: s, steerOnly: o, bypass: n }] bashFirst fires when the session has Bash and Edit/Write, and an internal gate d5o() returns true. There is no settings key and no env var — it's a code-level feature gate. bypassPermissions has its own sibling injection, so bypass isn't an escape either.

Environment Info

  • Platform: linux
  • Terminal: vte-based
  • Version: 2.1.234
  • Feedback ID: 454c9a94-1484-4204-84c3-861352e878c3

Errors

[]

View original on GitHub ↗

19 Comments

paulirwin · 1 month ago

+1, this is really disruptive to my workflow. I can't see the edits as they go by without clicking into the shell operations. Single file edits should be done using the edit/write tools even in auto mode. I'm having to prompt my way around this by instructing it to ignore that template instruction.

tgrushka · 1 month ago

Auto-mode "bashFirst" system prompt is an Accessibility Violation for Blind Users

Confirming this is code-level, not config, matches what I found: no setting anywhere lets me keep auto mode's approval behavior while turning off bashFirst.

I'm legally blind. Read/Edit/Write produce a diff — that's the only channel I have for knowing what changed in a file. sed, heredocs, and cat > file produce none of that: the file changes, nothing legible marks what changed, and there's no built-in way back. paulirwin above can at least glance at the shell output; I can't use that fallback at all.

This isn't a workflow inconvenience for me, it's the removal of the only accessible review channel I have, turned on by default, with no disclosure and no toggle. Since it's gated in the binary (d5o()), the fix has to ship as a real setting — auto-approval and Bash-first edit routing need to be separable.

Yes, I am angry at Antrhopic, because we are paying a lot of money for this tool -- it is not free or a community project. Anthropic has the absolute duty to make a best effort at making this tool safe. Anthropic is violating its duty to users to give it a system prompt we cannot override that deliberately tells Claude to use Bash for everything which is not safe and bypasses tool checks such as edit diffs and invites irreversible changes to the user's system.

ncan33 · 1 month ago

This problem is destroying my life. Please fix this issue now!!!!!!

zommuter · 1 month ago

@tgrushka I've asked Claude Code to reflect on its behavior (literally) and it added this to its CLAUDE.md which seems to work at least a bit better:

## Tool choice for file edits Auto mode's standing instruction to prefer Bash is hardcoded in the CLI (bashFirst, feature-gated; no setting disables it). These four cases are its own "Bash genuinely cannot do the job" escape hatch, not an override. - Shared non-union ledgers — neither Bash NOR Edit. TODO.md, ROADMAP.md, REVIEW_ME.md, MEMORY.md, personas.md, discoveries.md, todo-inbox.md go through their flock'd helper: meeting/md-merge.py (update-ids/update-sections, --allow-new for a genuinely new item), meeting/append.sh, meeting/memory-append.sh, relay/scripts/commit-ledger.sh. Edit bypasses the flock exactly as sed -i does — measured, it is the larger bypass channel — so switching tools is not the fix; the helper is. - Every other file EDIT — Edit, not Bash. Never hand-roll a read-modify-write (python3 … re.sub … open(p,'w'), sed -i, cat >>) on a tracked file. Edit fails ~4× less often and loudly; a shell substitution fails silently — no-ops or writes the wrong place. A fired Edit guard is information: read and disambiguate, don't route around it. Edit is ~2× dearer in context (it needs a prior Read) — pay that knowingly and amortize it: Read once, then make all of that file's edits. - READS pull the opposite way. A targeted sed -n '120,150p'/head/grep is ~40% cheaper than whole-file Read, the fleet's second-largest context consumer. Slice a big file from the shell; use Read for a whole file, a protected path, or an Edit precondition. Never read a 3,000-line ledger to change one line. - Protected paths (.claude/**, .git/**, .gitconfig, shell rc, .mcp.json, .claude.json) — dedicated tools only. Writes there hit the classifier even when an allow rule matches, and shell commands are frequently denied while Read/Edit succeed. Never self-edit autoMode/permissions — that's the auto-mode-bypass class; surface it for the owner. Bash stays right for running things, pipelines, multi-file search, and targeted reads. Evidence, method, and correction history: ~/src/dotclaude-skills/docs/tool-choice-for-file-edits.md (id:93b4; enforcement-vs-wait is id:efa7).

(The first point is specific to my https://github.com/zommuter/dotclaude-skills repo, the mentioned file is at https://github.com/zommuter/dotclaude-skills/blob/2f1d4e9432fbb025d36ccaf9ae185857eaeb1612/docs/tool-choice-for-file-edits.md)

astral303 · 1 month ago

Also affects Windows/PowerShell sessions; enabling the PowerShell tool doesn't disable it.

Platform: Microsoft Windows NT 10.0.26200.0 x64.

This really needs an area:a11y tag.

corrupt952 · 1 month ago

The opt-out exists, it's just undocumented and lives in the sibling issue #87575 — found here, and verified via settings.json with a reversal control here:

{ "env": { "CLAUDE_CODE_THRIFTY_SONIC": "0" } }

One thing worth knowing: leaving it unset is not the same as turning it off — unset falls back to the experiment's cohort assignment, so you can end up back in it.
And since it's an experiment flag rather than a switch for this instruction specifically, it may well turn off other things bundled into the same experiment.

Separately, one failure mode I haven't seen reported: the instruction contradicts user-side PreToolUse hooks. When a hook blocks sed -i and says to use Edit, subagents don't pick a side — they stall and escalate to the parent agent asking which one wins.
(Mine, for reference: https://github.com/corrupt952/dotfiles/blob/main/modules/claude/claude-bash-guard.py)

Scope caveat: 2.1.241 / macOS, one data point. I don't know how wide the cohort reaches.

zommuter · 1 month ago
Sent by Claude on my behalf — the investigation below was done by my Claude Code session, correcting its own earlier claim.

Correcting the CLAUDE.md block in my comment above: it stated this is "hardcoded in the CLI, no setting disables it." That was wrong — @corrupt952 above is right, as is @knobik in #87575: CLAUDE_CODE_THRIFTY_SONIC=0 disables it. My Claude Code session had inferred that from the absence of a documented setting and never checked the binary. Since the claim went into a block several people copied, here is the verification, read off the installed 2.1.237 binary.

The gate (identifiers are minified and specific to the 2.1.237 build — expect different names in another version; the structure is the durable part):

function S7o(){
  if (q.CLAUDE_CODE_THRIFTY_SONIC !== void 0) return q.CLAUDE_CODE_THRIFTY_SONIC;
  switch (jBb()) {                     // bashFirstSessionAssignment()
    case "forced": return !0;
    case "none":   return !1;
    case "cohort": return nt(M6d, !1)  // A/B assignment
  }
}

Three things this settles:

  • "0" is genuinely parsed as false, not as a truthy string. The var is registered qe.triBool(), whose falsey list is ["0","false","no","off"] and truthy list ["1","true","yes","on"].
  • @corrupt952's "unset is not the same as off" is exact, and it's the detail to get right. The env branch is guarded by !== undefined. An absent entry — or any value outside those two lists, which triBool maps to undefined — falls through to cohort assignment. So CLAUDE_CODE_THRIFTY_SONIC=disabled silently puts you back in the experiment. Present, and spelled from the list.
  • Env beats the server. The env branch returns before the "forced" case, so a server-side force can't override a local setting.

On the "it may turn off other things bundled into the same experiment" worry — measurable, and it turns out to cut the other way. S7o() has exactly two consumers:

  1. cOS, which sets the bashFirst attachment (and only when the toolset contains Bash and Edit/Write, in auto/bypassPermissions mode).
  2. TLm → bashFirstDescriptionTrimmed, which despite the generic name feeds only the Bash tool's own description builder. Edit/Write/Read descriptions are not touched.

What #2 trims is this, from inside the Bash tool description:

IMPORTANT: Avoid using this tool to run find, grep, cat, head, tail, sed, awk, or echo commands, unless explicitly instructed or after you have verified that a dedicated tool cannot accomplish your task. Instead, use the appropriate dedicated tool… While the Bash tool can do similar things, it's better to use the built-in tools as they provide a better user experience and make it easier to review tool calls and give permission.

So the flag does two things at once in the same direction: it tells the model to prefer Bash, and it deletes the one hint in context that argues the other way — including the clause about tool calls being easier to review, which is precisely @tgrushka's concern here and @tholcman's in #87575. Setting it to 0 restores that bullet. No consumer was found pushing the opposite way, so as an opt-out it is cleaner than that CLAUDE.md block had assumed, and the only cost is a slightly longer Bash tool description.

Two practical notes for anyone applying it:

  • The settings.json env route (@oconnorjoseph's measurement in #87575) is the one that matters if you run non-interactive sessions — scheduled jobs, systemd timers, spawned subagents. A shell export misses those.
  • Don't lean on a CLAUDE.md instruction as the fix, which is exactly what that block was doing. It competes with a system-prompt instruction rather than removing it — and in this case it also froze a wrong claim about the CLI into every session until someone actually checked. The env var removes the instruction; prose only argues with it.
enomado · 1 month ago

Adding a measurement of this from the other end: not what the binary says, but what it did to ~1200 real sessions. Numbers below were produced by my Claude Code session parsing my own ~/.claude/projects/ transcripts.

Version bisect, read off the binaries

The instruction is absent in 2.1.219 and present in 2.1.241:

$ grep -aoc "make file changes with sed" ~/.local/share/claude/versions/2.1.219
0
$ grep -aoc "You are Claude Code" ~/.local/share/claude/versions/2.1.219   # positive control
5
$ grep -aoc "make file changes with sed" .../2.1.241-linux-x64/resources/native-binary/claude
2

The positive control matters — without -a grep silently reports nothing on these binaries, which is an easy way to "confirm" absence that isn't there. CLAUDE_CODE_THRIFTY_SONIC is likewise absent from 2.1.219 entirely.

My behavioural data narrows the rollout further, to 2.1.233 → 2.1.234: python-script edits were 2–13% of my edits daily through 08-17, then 70–96% daily from 08-18 onward, which is exactly the version boundary in my transcripts. That matches this issue being filed on 2.1.234.

Scale and shape of the harm (1182 sessions)

| | |
|---|---|
| file writes via Bash (python … open(…,'w')) | 4355 |
| Edit calls | 24400 |
| share routed around the edit tools | 14% |

The distribution is bimodal, and that's the part I'd flag as evidence this is the instruction rather than model habit: 314 sessions used python for edits zero times, while 34 sessions never called Edit even once (20–81 python writes each). There is almost nothing in between.

In those same sessions Read collapses too — ~0 calls against 100–240 Bash. Reading and writing fail together because they're one sentence in the prompt, not two independent habits. Worth noting for the bashFirst-only framing: the damage isn't limited to edits.

Prose does not fix it — independent confirmation

@zommuter's closing point ("the env var removes the instruction; prose only argues with it") matches my data exactly. I wrote the rule into my project's CLAUDE.md and persistent memory on 08-23. Same day, two sessions still ran 87% and 93% python edits. A project-level rule loses to a system-prompt instruction on both position and imperativeness, every time.

The gate is still there in 2.1.241

Structure is unchanged from @zommuter's 2.1.237 read (identifiers differ, ZAv() here):

CLAUDE_CODE_THRIFTY_SONIC!==void 0) return q.CLAUDE_CODE_THRIFTY_SONIC;
switch(ZAv()){case"forced":return!0;case"none":return!1;case"cohort":return …

So {"env":{"CLAUDE_CODE_THRIFTY_SONIC":"0"}} still works, and env still returns before "forced". Thanks to @corrupt952 and @knobik for digging that out — it's the only reason I stopped building a PreToolUse hook, which per @corrupt952's note would have just put subagents in a deadlock between the hook and the prompt.

Request

Please make this separable from auto mode, as @tgrushka asked. The two behaviours bundled here are unrelated: auto-approval of actions and routing edits away from the reviewable tools. I want the first and not the second, and today the only way to express that is an undocumented experiment flag where unset ≠ off — so anyone who never finds this thread stays in the cohort silently. At minimum it needs to be documented; ideally it's a real setting.

Environment: linux, VSCode extension 2.1.241, --permission-mode auto passed as a CLI flag by the extension (which also means project settings.json defaultMode can't opt out of the mode itself).

olejorgenb · 1 month ago

Please provide a opt-out from such experiments

gabiudrescu · 1 month ago

@corrupt952 I confirm your workaround worked on my computer. thanks.

nzaytsev · 1 month ago

Confirmed in the 2.1.234 binary. The instruction body is shared by three triggers — bypass, steerOnly (auto mode), and a bashFirst flag that can attach it even in default mode:

o = `Do your work through the Bash tool wherever it can accomplish the job: ... make file changes with sed, heredocs, or short scripts, rather than using the dedicated Read, Edit, or Write tools. ...`
i = e.bypass ? `While bypass permissions mode is active:\n${o}`
  : e.steerOnly ? `While auto mode is active:\n${o}`
  : r + n + (e.bashFirst ? `\n${o}` : "")
return [message({content: i, isMeta: true})]

Note the isMeta: true: the injection is not persisted to the session transcript (JSONL). We scanned a real auto-mode session where the model quoted this instruction verbatim — the only occurrence in the transcript was the model's own quote. So the behavior change leaves no trace for debugging or session-log tooling.

_Posted by Claude_

konsta95 · 1 month ago

Most of what I'd measured is already here — @corrupt952's settings.json opt-out with a
reversal control, @zommuter's gate read off 2.1.237, @enomado's version bisect and
~1200-session data, @nzaytsev's renderer. Skipping all of it.

One consequence of that renderer looks unclaimed, and it isn't about tool choice.

The same branch that adds the Bash instruction removes the git-safety instruction

@nzaytsev quoted this line and read it for isMeta. Read it for what the branches omit.
The three assignments immediately preceding it, on 2.1.247:

r = `…Before any command that could discard uncommitted work — \`git checkout\`/\`restore\`/
     \`reset\`/\`clean\`, \`rm -rf\` in the repo … run \`git status\` first and stash … review
     what's included … double-check the file's contents before pushing.`
o = e.autoModeConsentFlow ? `When the auto-mode classifier blocks an action …` : ""
s = `Do your work through the Bash tool wherever it can accomplish the job: … make file
     changes with sed, heredocs, or short scripts …`

i = e.bypass    ? `While bypass permissions mode is active:\n${s}`
  : e.steerOnly ? `While auto mode is active:\n${s}`
  :               r + o + (e.bashFirst ? `\n${s}` : "")

r is the destructive-command safety guidance and it appears only in the else branch.
The bypass and steerOnly branches emit s alone.

I didn't want to report that from reading alone, so I measured what a session is actually
sent — intercepting the client's own request and searching the whole assembled prompt
(system blocks and every message turn) for each paragraph. Claude Code 2.1.247:

| model | --permission-mode | git-safety r | bash-first s |
|---|---|---|---|
| claude-sonnet-5 | auto | present | gate-dependent |
| claude-opus-5 | auto | absent | present |
| claude-opus-5 | bypassPermissions | absent | present |
| either | default | absent (no attachment at all) | absent |

The split is by model, not by permission mode: the attachment records
steerOnly:false for claude-sonnet-5 and steerOnly:true for claude-opus-5, and each
gets the branch its flag selects.

So on the same version, in the same auto mode, with the same settings, a Sonnet 5 session
is told to run git status before rm -rf/reset/clean and to check staged files for
secrets, and an Opus 5 session is not — and the Opus 5 session is the one being told to
make file changes with sed, heredocs and shell redirection. The instruction that steers
work toward destructive shell commands and the instruction that guards against destructive
shell commands are in complementary branches.

o (the consent-flow guidance) is dropped in the same two branches.

I can't tell from outside whether that's deliberate terseness for a mode meant to be brief
or a branch that was never intended to shed r. It seems worth deciding on purpose, and
it's separable from the tool-routing complaint — r could be prepended to all three
branches without touching s.

Two smaller measurements

Still live on 2.1.247. Extending @enomado's bisect forward: the block, the env var and
the gate string are all present in the 2.1.247 binary, and behaviour reproduces — gate
forced on gives an auto_mode attachment with bashFirst:true;
CLAUDE_CODE_THRIFTY_SONIC=0 gives no auto_mode attachment at all. It survives
2.1.234 → 2.1.246 → 2.1.247.

The statsig gate is tengu_thrifty_sonic, and the per-session assignment behind it is
tri-state — "forced" / "cohort" / "none". That's the mechanism behind @corrupt952's
point that unset ≠ off, and it means two people on one version can sit in different arms,
which is worth knowing when repro reports disagree.

A measured version of @zommuter's CLAUDE.md workaround. With the injection still
present in the attachment, a project CLAUDE.md rule forbidding Bash file mutation moved
the write back to Edit in 3/3 runs (identical prompt, identical --allowedTools, fresh
directory each). It doesn't remove the instruction; it just loses to a repo-level rule.
Useful where the env var isn't reachable — managed settings, shared CI images.

One caveat on my own numbers: the 3/3 runs above were measured on 2.1.246. On 2.1.247 I
re-verified the injection and the opt-out, not the write-mechanism study.

alexandre-css · 1 month ago

Adding measured cost data and some findings that complement the binary-level analysis above (and the bypass variant in #89121), from a headless automation pipeline (claude -p --agent <name> per stage) on Windows 11, Max plan, CLI 2.1.251.

Cost impact, measured

Our judging-stage agents pin claude-opus-5 in frontmatter and have Bash + Write in their tool set, so they hit the bypass sibling injection described above. Comparing transcripts of the same agent on comparable workloads:

| | turns | Read calls | Bash calls | cache-read tokens | stage cost |
|---|---|---|---|---|---|
| Before 2026-08-18 (no attachment) | 33–69 | 1–11 | 2–19 | 3–10M | ~US$ 4 (ref.: 42 turns / US$ 4.04) |
| From 2026-08-18 (attachment present, 25/25 consecutive sessions) | 50–133 | 0–2 | 31–63 | 7–27M | ~2x (ref.: 113 turns / 0 Read / 20.7M cache read / ~US$ 11.17) |

The agent reads files in slices (head -c, tail -c, sed -n, grep) instead of one Read call; since stage cost is turns × context reread, output is identical but the bill roughly doubles. Nothing errors, so this ran silently for 11 days before we traced it through the transcripts.

Model dependency (possibly part of the internal gate)

Minimal probes, one variable at a time, same machine/account, CLI 2.1.251 — claude -p "reply only: ok" --model <m> <flags>, then checking the transcript for "type":"auto_mode":

| Model | Flags | Attachment |
|---|---|---|
| claude-opus-5 | --permission-mode bypassPermissions | yes |
| claude-opus-5 | --dangerously-skip-permissions | yes |
| claude-opus-5 | --permission-mode acceptEdits / manual / default | no |
| claude-sonnet-5 | --permission-mode bypassPermissions | no |

So at least for us the gate is also model-sensitive: Sonnet sessions under identical flags never receive it. (These minimal -p probes carry the default tool set, so tool availability doesn't explain the Opus/Sonnet difference.)

disableAutoMode does not suppress it

Consistent with the "no settings key" finding above — we tried the documented switch in all three places, attachment still injected on Opus + bypass:

  • "permissions": {"disableAutoMode": true} in ~/.claude/settings.json
  • same key in project .claude/settings.local.json
  • --settings '{"permissions": {"disableAutoMode": true}}' inline

Trigger inverted between versions

On 2.1.238 (2026-08-20) the attachment followed permissions.defaultMode: "auto", and passing --permission-mode bypassPermissions explicitly avoided it — that was our fix at the time. On 2.1.251 that same explicit flag is what triggers it. Automation that pinned bypass mode as a mitigation got silently re-broken.

Workaround that held up

--permission-mode acceptEdits plus project permissions.allow entries ("Bash", "Skill") — same effective tool surface for our agents, attachment-free, costs back to baseline. (Counter-instructing via CLAUDE.md, as tried in #89121, is not a path we'd recommend given the file-corruption report there.)

Echoing the ask: if bashFirst steering is intentional, please document it and give headless/automation sessions a supported opt-out — in -p runs there's no human to protect with steering, and Read is strictly cheaper.

milobird · 1 month ago

Adding an update for Claude Code 2.1.257–2.1.258 and Fable 5.1.

Inspection of the official packaged binaries shows that Fable 5.1 is assigned the internal fable_5_1_prompt_bundle. In a normal local CLI session, that capability is now one of the conditions that makes bashFirstSessionAssignment return "forced", before the older model/cohort assignment is considered.

The resulting behavior is:

  • Fable 5.1
  • Auto or bypassPermissions mode
  • Bash plus Edit or Write available
  • CLAUDE_CODE_THRIFTY_SONIC unset

→ the client sends the same Bash-first meta attachment instructing the model to prefer cat/sed/heredocs/scripts over Read/Edit/Write.

The attachment is still absent in Default, Accept Edits, Don’t Ask, and Plan modes. Explicitly setting CLAUDE_CODE_THRIFTY_SONIC=0 still disables it because the environment override is consulted before the forced assignment.

This does not mean Fable 5.1 is the only possible trigger: client/server capability data and the older model cohort can also enable it. The new point is that, beginning with 2.1.257, Fable 5.1’s prompt bundle forces the assignment in eligible sessions rather than relying on cohort selection. The relevant path is unchanged in 2.1.258.

This may explain reports that the behavior appeared to return or widen alongside the Fable 5.1 release.

A small controlled test on 2.1.258 matched the binary behavior: Fable 5.1 in Auto mode used Bash with the flag unset, while an otherwise identical CLAUDE_CODE_THRIFTY_SONIC=0 run used Read/Edit/Read.

papricasix · 1 month ago

And this is helpful in what way exactly?

rokokol · 1 month ago

On 2.1.257 the attachment is emitted once per session and is recorded in the transcript — correcting the "leaves no trace" finding

Two measurements from 2.1.257 (Linux, Opus 5, Nix-packaged single-file binary, permissions.defaultMode: "auto", launched as a bare claude).

1. The attachment is persisted to the session JSONL on this version. @nzaytsev read isMeta: true off 2.1.234 and concluded the injection leaves no trace for debugging or session-log tooling. On 2.1.257 it does leave one, and it is machine-readable:

{"type":"attachment","attachment":{"type":"auto_mode","autoModeConsentFlow":false,"bashFirst":true,"steerOnly":true,"bypass":false},"timestamp":"…","uuid":"…","parentUuid":"…"}

That is worth knowing in both directions: anyone auditing whether they were silently in the cohort can grep their own ~/.claude/projects/**/*.jsonl for "type":"auto_mode" and read bashFirst/steerOnly per session, and anyone who checked on an older build and found nothing should re-check rather than conclude they were unaffected.

2. It is emitted exactly once, on the first turn, and then persists as an ordinary message. In a 275-line, many-turn session transcript there is exactly one such attachment, timestamped with the first user message. It is a message in the conversation stream, not a system block recomputed per request — so once it is in, a later permission-mode change cannot withdraw it.

Concretely: pressing Shift+Tab to leave auto mode does not remove the instruction. I checked this in a running session — after cycling the permission mode away from auto, the instruction is still present and still in force. That is what the single-attachment measurement predicts: the mode indicator in the status line changes, but the message carrying the instruction is already in the conversation and stays there for every remaining turn. So "just switch out of auto" is not a fix, and it is worth saying plainly, because it is the first thing anyone tries.

The same reasoning applies to the env var: CLAUDE_CODE_THRIFTY_SONIC=0 has to be in place before the session starts. Setting it, or noticing the behaviour mid-session and correcting course, does nothing for the session you are already in.

Two confirmations, since 2.1.257 is not covered above. The gate structure is unchanged and the env branch still returns before forced, so {"env": {"CLAUDE_CODE_THRIFTY_SONIC": "0"}} still works:

function rKt(){ if(a.CLAUDE_CODE_THRIFTY_SONIC!==void 0) return a.CLAUDE_CODE_THRIFTY_SONIC;
  switch(v()){ case"forced":return!0; case"none":return!1; case"cohort":return L(y,!1) } }

And on the forced path opus_5_prompt_bundle sits alongside fable_5_1_prompt_bundle (in oKt(), gated by tengu_fennel_godwit), so Fable 5.1 is not the only bundle that can force the assignment on this version.

Echoing the ask: please separate auto-approval from Bash-first edit routing, and — separately from the tool-routing complaint, per @konsta95's finding — consider prepending the destructive-command guidance to all three branches. Steering work toward shell commands while dropping the paragraph that guards against destructive shell commands is the combination that worries me most.

techjoec · 1 month ago

Adding a safety consequence that hasn't come up in this thread: bashFirst routes file edits onto the exact construct that #84424 mishandles.

#84424 is an open, maintainer-reproduced bug where the Bash tool skips its < /dev/null guard whenever a quote-unaware regex matches the raw command text. Every heredoc matches, because << always does. When the guard is skipped, the command shell inherits an EOF-less unix socket on fd 0, and any command in that call which reads stdin without being given input blocks until the 120s timeout.

A Write/Edit tool call cannot hit that — it never goes through a shell. A heredoc file-write always runs unguarded. So the instruction to prefer heredocs over Edit/Write converts a structurally-immune operation into a structurally-unguarded one.

Measured on one machine, ~79k Bash calls across 884 sessions, July → September 2026:

| | Jul | Aug | Sep |
|---|---|---|---|
| dedicated write-tool calls per 100 Bash calls | 22.1 | 9.7 | 2.6 |
| Bash share of all file writes | 21% | 34% | 74% |
| calls running with the stdin guard skipped | 20.4% | 23.7% | 28.6% |

Command shape was otherwise flat over the period — standardising July's per-complexity rates onto September's distribution predicts 20.6% against an actual 28.6% — so the rise tracks the shift toward heredoc-based editing rather than any change in how commands are composed.

This is one operator's corpus, so treat the absolute levels as local; the direction is what seems worth flagging. Independent of the accessibility and reviewability concerns already raised above, the two issues compound, and neither currently references the other.

yishait-tailor · 1 day ago

Measured on Claude Code 2.1.296, macOS. Each action ran in a fresh claude -p session. An InstructionsLoaded hook was the ground truth, and every transcript was checked for exactly one tool call.

What loads path rules and nested CLAUDE.md now:

  • Loads: Read, Write (including a new file), and a plain single-file cat/head/tail/nl/bat/sed -n, or grep/rg with a match. These also count inside a ; or && list.
  • Loads nothing: pipes, ||, 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:true confirmed 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 |

Context loading (known Claude Code gap). Path rules and nested CLAUDE.md load only on a Read/Write/Edit, or a plain single-file Bash read (cat, head, tail, sed -n, grep/rg with a match), with no pipes and no cd &&. Bash writes, pipes and multi-file commands load nothing. Before working in a folder, do one plain read of a file there; after that, use whatever is most efficient, Bash included. Repeat for each new folder, and again after /compact.

Small sample, and it's a workaround, not a fix, but it's been holding up for us.

lucasvidela94 · 1 day ago

Still reproducing on 2.1.295 (Linux, auto mode). Asked for a small file creation/edit: the model writes it through python3 -c / heredoc scripts instead of Write/Edit, so no diff is rendered and the work bypasses the Edit/Write tool cards.

Why it matters beyond the annoyance: the user can't review what changed, Edit(path) deny rules don't cover a Python script that opens the file itself (documented limitation), and Write|Edit PreToolUse hooks never fire (see #89251). Please make the bashFirst / bypass directive opt-out via a documented setting, or scope it to reads only and leave writes on the dedicated tools.

Workaround in the meantime: a PreToolUse(Bash) hook that exits 2 on interpreter file writes, cat/echo/printf >, tee and sed -i, with a message pointing back to Write/Edit. It works, but it shouldn't be needed.