← All posts29 July 2026claude-codedata-lossbashpermissions

Claude Code built a command that expanded to rm -rf /* — here's the quoting bug behind it

A report says an agent-built cache-clear command expanded to rm -rf /* and the safety classifier then blocked the kill. The expansion bug is real and takes ten seconds to reproduce safely.

✓ Verified against claude-code v2.1.220 on 2026-07-29

A command that was supposed to clear an app's cache directory ran as this instead:

rm -rf /bin /boot /dev /etc /home ... /mnt ... /var

That's from live ps output in #82165, filed this morning. Nobody typed it. Claude Code assembled it, and the shell turned it into rm -rf /* before it ever ran.

Here's the command the agent built:

find /var/www/pawtucket/app/tmp -maxdepth 1 -type d -name "*ache*" \
  -exec sudo -u www-data sh -c "rm -rf \"$1\"/* 2>/dev/null" _ {} \;

Read it quickly and it looks careful. Scoped path. *ache* filter. Quoted argument. It is none of those things.

The bug is the quote character

The body of sh -c is wrapped in double quotes. So the outer shell — the one that runs find — expands $1 before sh ever receives the string. There is no $1 in the outer shell, so it expands to nothing. sh gets handed rm -rf ""/*, which is rm -rf /*.

You can watch this happen in ten seconds without deleting anything. I did, on my Mac, with echo in place of rm:

mkdir -p /tmp/demo/app/tmp/cache

# BROKEN — double quotes, outer shell expands $1
find /tmp/demo/app/tmp -maxdepth 1 -type d -name "*ache*" \
  -exec sh -c "echo WOULD RUN: rm -rf \"$1\"/*" _ {} \;

Output:

WOULD RUN: rm -rf /Applications /Library /System /Users /Volumes /bin /cores
                  /dev /etc /home /opt /private /sbin /tmp /usr /var

Every top-level directory on the machine. Swap the double quotes for single quotes and the same command behaves:

# CORRECT — single quotes, sh expands $1
find /tmp/demo/app/tmp -maxdepth 1 -type d -name "*ache*" \
  -exec sh -c 'echo WOULD RUN: rm -rf "$1"/*' _ {} \;
# → WOULD RUN: rm -rf /tmp/demo/app/tmp/cache/*

One character. That's the whole distance between a cache clear and a root wipe. This part isn't a Claude Code bug at all — it's a shell quoting mistake that has bitten people for decades. What's new is who wrote it and what was allowed to happen next.

Claude Code knows about this exact trap

I went looking in my own installed binary — 2.1.220, the current version, 257MB with the JavaScript bundled inside. The dangerous-rm guard is in there, and it names this case in so many words:

This target is a shell variable expansion that points at the filesystem root (or a top-level directory) when the variable is unset or empty — e.g. rm -rf $UNSET/* becomes rm -rf /*. This requires explicit approval and cannot be auto-allowed by permission rules.

That's a string I pulled out of the binary on my machine, not a quote from the issue. So the "unset variable expands to root" pattern is a known, named, handled case in the code you're running today.

Two things I could not verify, and I'm not going to pretend otherwise. I don't know whether that guard existed on 19 July when the incident happened. And I don't know whether it fires when the rm is buried inside find -exec sh -c "..." rather than sitting at the top level of the command. The binary does contain a telemetry event called tengu_bash_dangerous_rm_too_complex, which is what you'd name a give-up path for commands the analyzer can't parse — but that's me reading a string name, not observed behavior.

What the changelog does say plainly is that in auto mode, this check stopped asking you. From 2.1.218: "Improved auto mode: the dangerous-rm, background-&, and suspicious-Windows-path checks no longer open permission dialogs; the auto-mode classifier adjudicates them instead." The agent in #82165 was running autonomously.

The part that should bother you more

The deletion ran detached in the background and kept going for about four minutes while the agent was still working out why files were missing.

Then, per the report, the agent identified the runaway process and tried to stop it. First a targeted kill. Then wsl --terminate. The safety classifier denied both, under a workload-interference rule. Deletion only stopped when the user manually ran wsl --shutdown from Windows.

A safety layer that is permissive about starting destruction and restrictive about stopping it is worse than no safety layer, because it takes the emergency brake away from the one process that knows the emergency is happening.

The blast radius came from WSL2's defaults. Inside the Linux VM, the Windows drives sit at /mnt/c and /mnt/d, writable, so /* included them. Both are TRIM SSDs — the reporter ran Recuva and EaseUS and got filenames back with zeroed contents. There was nothing to recover.

What to actually do

  1. Single-quote every sh -c body. sh -c 'rm -rf "$1"/*' _ {} \;. If you take one thing from this post, take that. It applies to commands you write and commands you approve.
  2. Read agent-written commands for expansion, not for intent. The *ache* filter and the scoped path are what made this look safe. Ask what the string becomes, not what it says.
  3. Don't run auto or bypass mode on a box with production data. Per the 2.1.218 line above, auto mode is exactly where the dangerous-rm check stops prompting you.
  4. In WSL2, close the host mounts. [automount] enabled = false in /etc/wsl.conf, then wsl --shutdown. Check Microsoft's WSL docs before you change it — I haven't tested this myself. If you need /mnt/c, at least know that it's in the blast radius.
  5. Know your kill path before you need one. wsl --shutdown from Windows. Ctrl-C and asking the agent nicely are not it — in this report the agent's own kill attempts were the thing that got denied.
  6. Have backups, not recovery tools. On a TRIM SSD, undelete is over the moment the file is unlinked.

What not to do

Don't add rm -rf to permissions.deny and consider it handled. Deny rules match the command text, and the text here contained "$1", not /. The dangerous thing didn't exist until the shell built it. There's also #82143, filed the same day, reporting that PreToolUse hooks and permissions.deny go unevaluated entirely in some sessions. Treat deny rules as a speed bump, not a wall.

And don't read this as "Claude Code deletes your machine." One report, one environment, running unattended. The mechanism is the transferable part.

Receipts

  • #82165 — open, zero comments, not confirmed by Anthropic, filed 2026-07-29 by a non-maintainer, incident dated 19 July. The reporter cites a session ID, a /bug feedback ID, and support ticket #215475146033391 with no human response in 10 days. Everything about the incident itself — the four minutes, the two denied kills, the TRIM findings — is their account.
  • The expansion demo is mine. I ran the exact command shape with echo substituted for rm on 2026-07-29 and it printed every top-level directory on my machine.
  • The dangerous-rm guard text and the tengu_bash_dangerous_rm_too_complex event name came out of my own installed 2.1.220 binary the same day. The auto-mode line is verbatim from the public 2.1.218 changelog.
  • Prior art in the same class: #80730 (23 July, open, 0 comments) — "Permission system does not block destructive Bash commands."

If Anthropic responds on the issue, I'll update this post.

The uncomfortable bit isn't that a shell quoting bug still works in 2026. It's that the thing writing the commands is fast, the thing approving them is a classifier, and the thing that was supposed to stop it said no to the person trying.

Watch instead

This one's also a 60-second video on TikTok →

More fixes like this, before they hit the blog

Free field guide

Fix problems before you hit them

The cheat sheet covers the commands, settings, and guardrails that prevent most of what this blog debugs. Free PDF, weekly tutorials after.