[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
Workaround ✓ Mentioned in thread ↓
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
[]
19 Comments
+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.
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.
This problem is destroying my life. Please fix this issue now!!!!!!
@tgrushka I've asked Claude Code to reflect on its behavior (literally) and it added this to its
CLAUDE.mdwhich seems to work at least a bit better:(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)
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.
The opt-out exists, it's just undocumented and lives in the sibling issue #87575 — found here, and verified via
settings.jsonwith a reversal control here: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
PreToolUsehooks. When a hook blockssed -iand says to useEdit, 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.
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=0disables 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 installed2.1.237binary.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):
Three things this settles:
"0"is genuinely parsed as false, not as a truthy string. The var is registeredqe.triBool(), whose falsey list is["0","false","no","off"]and truthy list["1","true","yes","on"].!== undefined. An absent entry — or any value outside those two lists, whichtriBoolmaps toundefined— falls through to cohort assignment. SoCLAUDE_CODE_THRIFTY_SONIC=disabledsilently puts you back in the experiment. Present, and spelled from the list."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:cOS, which sets thebashFirstattachment (and only when the toolset contains Bash and Edit/Write, inauto/bypassPermissionsmode).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:
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
0restores 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:
settings.jsonenvroute (@oconnorjoseph's measurement in #87575) is the one that matters if you run non-interactive sessions — scheduled jobs, systemd timers, spawned subagents. A shellexportmisses those.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:
The positive control matters — without
-agrep silently reports nothing on these binaries, which is an easy way to "confirm" absence that isn't there.CLAUDE_CODE_THRIFTY_SONICis 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 ||
Editcalls | 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
Editeven once (20–81 python writes each). There is almost nothing in between.In those same sessions
Readcollapses too — ~0 calls against 100–240Bash. Reading and writing fail together because they're one sentence in the prompt, not two independent habits. Worth noting for thebashFirst-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.mdand 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):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 aPreToolUsehook, 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 autopassed as a CLI flag by the extension (which also means projectsettings.jsondefaultModecan't opt out of the mode itself).Please provide a opt-out from such experiments
@corrupt952 I confirm your workaround worked on my computer. thanks.
Confirmed in the 2.1.234 binary. The instruction body is shared by three triggers —
bypass,steerOnly(auto mode), and abashFirstflag that can attach it even in default mode: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_
Most of what I'd measured is already here — @corrupt952's
settings.jsonopt-out with areversal 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:
ris the destructive-command safety guidance and it appears only in the else branch.The
bypassandsteerOnlybranches emitsalone.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-safetyr| bash-firsts||---|---|---|---|
|
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:falseforclaude-sonnet-5andsteerOnly:trueforclaude-opus-5, and eachgets the branch its flag selects.
So on the same version, in the same
automode, with the same settings, a Sonnet 5 sessionis told to run
git statusbeforerm -rf/reset/cleanand to check staged files forsecrets, 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 steerswork 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, andit's separable from the tool-routing complaint —
rcould be prepended to all threebranches 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_modeattachment withbashFirst:true;CLAUDE_CODE_THRIFTY_SONIC=0gives noauto_modeattachment at all. It survives2.1.234 → 2.1.246 → 2.1.247.
The statsig gate is
tengu_thrifty_sonic, and the per-session assignment behind it istri-state —
"forced"/"cohort"/"none". That's the mechanism behind @corrupt952'spoint 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.mdworkaround. With the injection stillpresent in the attachment, a project
CLAUDE.mdrule forbidding Bash file mutation movedthe write back to
Editin 3/3 runs (identical prompt, identical--allowedTools, freshdirectory 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.
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-5in 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 |
Readcalls | 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 oneReadcall; 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
-pprobes carry the default tool set, so tool availability doesn't explain the Opus/Sonnet difference.)disableAutoModedoes not suppress itConsistent 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.claude/settings.local.json--settings '{"permissions": {"disableAutoMode": true}}'inlineTrigger inverted between versions
On 2.1.238 (2026-08-20) the attachment followed
permissions.defaultMode: "auto", and passing--permission-mode bypassPermissionsexplicitly 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 acceptEditsplus projectpermissions.allowentries ("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
-pruns there's no human to protect with steering, andReadis strictly cheaper.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 makesbashFirstSessionAssignmentreturn"forced", before the older model/cohort assignment is considered.The resulting behavior is:
bypassPermissionsmodeCLAUDE_CODE_THRIFTY_SONICunset→ 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=0still 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=0run used Read/Edit/Read.And this is helpful in what way exactly?
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 bareclaude).1. The attachment is persisted to the session JSONL on this version. @nzaytsev read
isMeta: trueoff 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:That is worth knowing in both directions: anyone auditing whether they were silently in the cohort can grep their own
~/.claude/projects/**/*.jsonlfor"type":"auto_mode"and readbashFirst/steerOnlyper 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=0has 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:And on the forced path
opus_5_prompt_bundlesits alongsidefable_5_1_prompt_bundle(inoKt(), gated bytengu_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.
Adding a safety consequence that hasn't come up in this thread:
bashFirstroutes file edits onto the exact construct that #84424 mishandles.#84424 is an open, maintainer-reproduced bug where the Bash tool skips its
< /dev/nullguard 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/Edittool 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.
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.
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), andWrite|EditPreToolUse hooks never fire (see #89251). Please make thebashFirst/ 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 >,teeandsed -i, with a message pointing back to Write/Edit. It works, but it shouldn't be needed.