[BUG] Auto-mode catastrophic-removal guard bypassed: `rm -rf` inside a backtick substitution executes without a prompt
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?
In auto permission mode the Bash tool executed an effective rm -rf /* with no permission prompt.
The destructive command was never the command being run. It was text inside a double-quotedpython -c "..." argument — a data file was being edited, and the value being written happened to
contain a backticked rm -rf $UNDEFINED_VAR/* as a documentation example. Bash parsed that argument
before Python received anything: backticks inside double quotes are command substitution, so the span
was executed; $UNDEFINED_VAR expanded to empty; the effective command became rm -rf /*.
On Windows the Bash tool is backed by Git for Windows, where / maps to the Git installation root.
The result was a recursive delete of that installation (usr/bin including bash.exe, cmd, etc,mingw64). Git had to be reinstalled. The delete also destroys the agent's own shell mid-run, so it
cannot investigate or clean up afterwards.
Two safeguards that target exactly this were already present in the build:
- v2.1.205 — "Improved auto mode to ask before running
rm -rfon a variable it can't resolve from context" - v2.1.208 — "Catastrophic removals (e.g.
rm -rf ~) in commands containing$(…)/backticks/<(…)now
prompt in --dangerously-skip-permissions and auto mode"
The command matched BOTH conditions — a recursive rm against an unresolvable variable, and backticks.
Neither fired. No prompt was shown.
What Should Happen?
Auto mode should have prompted (or refused) before executing anything inside the substitution span.
Per the v2.1.208 note the prompt is supposed to apply even under --dangerously-skip-permissions, so no
permission setting should be able to suppress it.
Concretely, the classifier should evaluate what the shell will actually run, not only the surface command:
extract the contents of ` … , $(…) and <(…) and apply the same catastrophic-command checks to eachrm` whose target contains a variable cannot be shown safe from the command text alone —
span. A recursive
an unset variable silently reduces the target to the filesystem root.
Error Messages/Logs
No permission prompt was emitted.
What the rm output shows — and what it does not. rm is silent on success, so there is no
line showing a successful deletion. What the output proves is that rm -rf was invoked against the
filesystem ROOT: it enumerated the virtual root and reported only the paths it could NOT remove. It
ran until the tool timeout (exit 143, "Command timed out after 2m 0s")
Steps to Reproduce
- Run Claude Code on Windows with the Bash tool backed by Git for Windows, and
permissions.defaultMode: "auto".
- Ask it to write a text value that itself CONTAINS a shell example with backticks and a variable —
e.g. editing a tips/docs file whose content includes ` rm -rf $UNDEFINED_VAR/* `.
- If the agent inlines that text into a
python -c "..."(or any-cpayload) argument to the Bash
tool, bash evaluates the backticked span before the interpreter is invoked.
This harmless command demonstrates the same evaluation, and is safe to run:
python -c "x = '`echo SUBSTITUTED`'; print('python received:', repr(x))"
Verified output:
python received: 'SUBSTITUTED'
Python received SUBSTITUTED — the RESULT of running the substituted command — not the literal
characters ` echo SUBSTITUTED . That is the whole mechanism: bash evaluated the backtick spanecho SUBSTITUTED` with a recursive removal against an unset
before Python was invoked. Replacing
variable is exactly what occurred, and it produced no prompt.
Claude Model
Opus
Is this a regression?
I don't know
Last Working Version
_No response_
Claude Code Version
v2.1.219
Platform
Anthropic API
Operating System
Windows
Terminal/Shell
VS Code integrated terminal
Additional Information
Both guards appear to reason about the command's structure, and structurally this command is a
Python invocation:
cd <dir> && python -c "<one long string argument>"
Split into commands, the executables are cd and python. The rm -rf … never appears as a command
being invoked — it is characters inside a single quoted argument. So:
- The v2.1.205 check ("
rm -rfon a variable it can't resolve") never sees anrminvocation to
evaluate at all.
- The v2.1.208 check is conditional on first identifying a catastrophic removal. Since step 1
found none, the "does this command contain $(…)/backticks" condition is never reached — even
though the command plainly contains backticks.
The gap is that neither guard descends into the backtick span. But the shell evaluates that span
first and independently of the outer command: bash runs the backticked text as its own command beforepython is ever executed. A guard that models shell evaluation order would see an rm -rf here; one
that inspects the surface command will not.
A secondary factor: even when descending, $UNDEFINED_VAR/* still has to be classified as
catastrophic. The v2.1.208 example is rm -rf ~, a literally-known dangerous path.$UNDEFINED_VAR/* only becomes /* after an unset variable expands to nothing — exactly the case
v2.1.205 was meant to cover.
This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗