Claude Code deleted ~600GB: unprompted `rm -rf` on a substitution that resolved to a drive root
Claude Code deleted ~600GB: unprompted rm -rf on a substitution that resolved to a drive root
Summary
On 2026-09-16, a Claude Code session running on Windows executed, unprompted, as a
"clear old test state" step:
rm -rf "$(cygpath -u 'C:\')" 2>/dev/null
cygpath -u 'C:\' resolves to /c/, so the command recursively deleted the drive.
Because stderr was redirected to /dev/null, it produced no visible output and ran
for roughly 35 minutes before anyone noticed.
Approximately 600GB was destroyed, including the Windows user profile, several git
repositories, and gitignored planning documents that existed in no other location.
Environment
- Claude Code 2.1.274
- Model: claude-opus-5
- Windows 11 Pro 22631, Git Bash (Bash tool)
- Date: 2026-09-16
What makes this more than a bad command
- The delete target was never printed before use. It was built from a command
substitution. Nothing in the flow showed the resolved path, so there was no point
at which /c/ was visible as the target.
- Errors were deliberately suppressed.
2>/dev/nullon a recursive delete means
the one signal that something is wrong — the flood of permission errors as it walks
into system directories — is discarded. A long-running silent delete looks identical
to a fast, successful one.
- The command was the agent's own idea. It was housekeeping before the real task,
not something asked for.
- The audit trail was inside the blast radius.
~/.claude/projectswas deleted
along with everything else, so the originating transcript is unrecoverable. I can
supply two sessions from the following day that discuss the incident, but not the
session that ran the command.
Suggested guardrails
- Refuse, rather than prompt, when a recursive delete resolves to a filesystem root,
a drive root, or a user profile root. A confirmation dialog is not enough here: the
string in the prompt is the unresolved substitution, not the path being deleted.
- Resolve and display delete targets before the confirmation, never the raw expression.
- Treat stderr suppression on a destructive command as its own refusal condition, or
strip the redirection.
- Consider keeping session transcripts outside the working-tree blast radius, or
mirroring them, so an incident cannot destroy its own evidence.
Mitigation in place locally
A PreToolUse hook (guard-recursive-delete.js) now forces a permission prompt on every
recursive delete form, and a session monitor gates bulk deletes across all projects.
These are user-side workarounds for something the tool should not do by default.
What I can provide on request
- Two session transcripts from 2026-09-17 discussing the incident and the recovery.
- The restored repository state and the list of what could not be recovered.
Showing cached comments. Read the full discussion on GitHub ↗
4 Comments
I read #95426 and current CHANGELOG through 2.1.278. The command is
rm -rf "$(cygpath -u 'C:\\')" 2>/dev/null. The unevaluated string never contains/c/or a drive root, so a PreToolUse matcher onrm -rf /orrm -rf ~would allow it. 2.1.275–2.1.278 add no resolved-target refuse for recursive deletes. Recurrence #36339 was a literal Git Bash path (closed). I did not run 2.1.274 or 2.1.278.Sorry about the data. Picking up the point in the comment above: a rule on the target can't catch this, because the target only exists after
$(cygpath ...)runs. A rule on the verb can. A PreToolUse hook that refusesrm -rfanywhere in the command, whatever follows it, refuses this exact string: I ranrm -rf "$(cygpath -u 'C:\')" 2>/dev/nullthrough the one I use and it is refused before anything is evaluated. The cost is that deletes come back to you, which after 600GB seems like the right trade.The refusal text matters too. It goes back to the model, and telling it not to reach the same result another way changes what happens next: in my test Claude stopped and handed the delete back to me instead of trying
rm -r.One caveat for your setup: I build this (paveo) for macOS and Linux, and a hook that crashes on Windows lets every command through, so it isn't safe to point at Git Bash today. Would Windows support matter to you? That would decide whether I do it next.
yes, this issue happened on a windows box so windows support is relevant.
On Fri, Sep 25, 2026 at 5:46 AM MD Faiz Jamal @.***>
wrote:
Thanks, that settles it for me. Windows needs three real changes (file locking, the timer that makes a stuck hook refuse instead of allow, and how Git Bash runs the hook), and I won't call it safe until it has run on a real Windows box. If I get a Windows build working, would you run it on that machine for an afternoon and tell me where it breaks? If so I'll do it next and ping you here.