[BUG] Auto-mode catastrophic-removal guard bypassed: `rm -rf` inside a backtick substitution executes without a prompt

Status Open
Maintainer reply None cached
Activity 1 comment · opened Jul 26, 2026

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-quoted
python -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 -rf on 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 each
span. A recursive
rm` whose target contains a variable cannot be shown safe from the command text alone —
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

  1. Run Claude Code on Windows with the Bash tool backed by Git for Windows, and

permissions.defaultMode: "auto".

  1. 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/* `.

  1. If the agent inlines that text into a python -c "..." (or any -c payload) 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 span
before Python was invoked. Replacing
echo SUBSTITUTED` with a recursive removal against an unset
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:

  1. The v2.1.205 check ("rm -rf on a variable it can't resolve") never sees an rm invocation to

evaluate at all.

  1. 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 before
python 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.

View original on GitHub ↗

This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗