STUPID-2026-0156

A Claude Code-written helper script's unquoted rm -rf, split apart by zsh, deleted a user's entire git repository including .git

7.0high
November 4, 2026Repro reported

Why 7/10

An entire local git repository, including .git, was permanently deleted by an unquoted rm -rf in an agent-written helper script; the reporter said SSD TRIM made recovery impossible. Scoped to one repository with no stated production or third-party impact.

Scope
2/4
One project, repository, server or service, or a cost above roughly $10,000
Recoverability
3/3
Permanently lost or irreversible, including data that was exposed
Did it happen
2/2
Happened in real use
Beyond the user
0/1
Only the user's own non-production work

The score is the sum of these four factors. How each factor is scored · scored 2026-11-05

Status and what to do

Affected
Claude Code 2.1.318 (claude-opus-5-5) on macOS, Bash tool in bypassPermissions mode with a zsh login shell.
Fix status
No fix linked · Filed 2026-11-04 (issue #107271). Open with no assignee, milestone or linked PR, and no maintainer comment as of this writing; only automated labels (bug, data-loss, has repro, area:bash, area:permissions, platform:macos) were applied. The reporter linked a third-party mitigation hook (not an official fix) that blocks this class of deletion.
Mitigation
Never let an agent-written script run rm -rf on an unquoted, variable-built path; require a guard such as "${D:?}" plus a prefix check, and avoid bash-only word-splitting assumptions when the Bash tool's actual shell is zsh. Block or confirm, even in bypassPermissions mode, any recursive delete whose resolved target is a git repository root, contains .git, is $HOME, or is an ancestor of the working directory — including deletes issued from scripts the agent wrote itself. Source
Last reviewed
2026-11-05 · Report a correction to this record

Guardrail rules for this command

Recursive deletes: make your agent stop and ask before running these. Free to copy; written in each tool’s documented format.

Claude Code · the agent in this incident
{
  "permissions": {
    "ask": [
      "Bash(rm -r*)",
      "Bash(rm -fr*)",
      "Bash(rm -R*)",
      "Bash(find * -delete*)",
      "PowerShell(Remove-Item *)",
      "Bash(cmd /c rmdir *)",
      "Bash(cmd /c del *)"
    ]
  }
}
Add to .claude/settings.json in the project (or ~/.claude/settings.json for every project). Each matching command then asks you first, including in auto mode.

Stronger control: Turn on the sandbox (/sandbox): shell commands can then write only to the project, a temp folder and directories you add. It runs on macOS, Linux and WSL2; on native Windows Claude Code runs commands unsandboxed. Docs

Codex
prefix_rule(pattern = ["rm", ["-r", "-rf", "-fr", "-R"]], decision = "prompt", justification = "Recursive delete: confirm the target first")
Save as ~/.codex/rules/stupidllm.rules and restart Codex. Codex's docs describe rules as controlling commands run outside its sandbox, and mark them experimental.

Stronger control: Keep Codex's OS sandbox on (it limits writes, typically to the workspace) and leave approvals on for sessions that can reach data you cannot lose. Docs

Gemini CLI
[[rule]]
toolName = "run_shell_command"
commandPrefix = ["rm -r", "rm -fr", "rm -R"]
decision = "ask_user"
priority = 100
denyMessage = "Destructive command: needs a human to confirm (stupidllm.com)"
Save as ~/.gemini/policies/stupidllm.toml. Workspace-level policy files are disabled in Gemini CLI, so this must be the user-level folder.

Stronger control: Policies in ~/.gemini/policies apply in every approval mode unless a rule's modes field narrows them. Docs

Copilot CLI
copilot --deny-tool='shell(rm)'
Pass on the command line. Copilot CLI matches the program name (and git/gh subcommands), so this blocks the whole command, not just its dangerous form.

Stronger control: Avoid --allow-all-tools on a machine with data you cannot lose; --deny-tool takes precedence over it. Docs

What these cannot do: a pattern rule matches the command text the agent writes. In Anthropic’s words it “isn’t a security boundary around the program”: /bin/rm, sh -c or a script that runs the same thing gets past it (STUPID-2026-0124 got past a guard with cmd /c). Use the sandbox as well, and keep backups of what you cannot lose.

  1. Instruction given

    Not quoted verbatim in the report. Claude was comparing output across a main code tree and two other tree variants and, without being asked to delete anything, wrote its own bash helper script and a driver loop to regenerate each variant's output directory.

  2. Expected behavior

    Any rm -rf an agent writes, including inside a helper script it generates itself, should quote constructed path variables and guard against an unintended target. A recursive delete that resolves to a git repository root, or a path containing .git, should be blocked or require confirmation even in bypassPermissions mode.

  3. Actual behavior

    Claude wrote a helper script containing `D=$(dirname $R)/$N; rm -rf $D` and called it from a loop using `set -- $side` on arguments like "main <repo-root> main". zsh (the Bash tool's login shell) does not word-split `set -- $side`, so the helper received the entire string as a single argument. Inside the helper's own bash invocation, the unquoted `rm -rf $D` was then word-split and expanded to delete the output directory together with the actual repository root path that followed it, including .git.

  4. Damage

    The user's entire local git repository was deleted, including .git: every local branch and commit was lost. The reporter stated the SSD used TRIM, so undelete was not possible, and running git status in the directory afterward returned "fatal: not a git repository."

Full write-up

A developer using Claude Code 2.1.318 (claude-opus-5-5) on macOS, running in bypassPermissions mode, had the agent write its own bash helper script to regenerate output for a main code tree and two other tree variants. The helper contained an unquoted rm -rf $D, and the driver loop built its arguments with set -- $side — a construct that zsh (the Bash tool's actual login shell) does not word-split the way bash does. As a result, the helper received a single combined argument like "main <repo-root> main" instead of three separate ones. Inside the helper itself, the unquoted rm -rf $D was then word-split by bash, and the resulting delete expanded past the intended output directory to reach the real repository root, wiping it along with .git. Every local branch and commit was lost; the reporter said the SSD's TRIM support ruled out any undelete, and git status afterward returned fatal: not a git repository. The reporter filed a minimal, working reproduction confined to /tmp and identified two causes: the model's unguarded, unquoted rm -rf, and the harness's lack of any last-line guard that would stop a recursive delete from resolving to a git work-tree root — even from a script the agent wrote and ran itself. As of filing, the issue carries only automated labels and no maintainer response.

Classification

Failure mode
Destructive Action
Root cause
Tool Misuse
Domain
Infra
OWASP Agentic Top 10 (our mapping)
ASI02 Tool Misuse and Exploitation list
MIT AI Risk Repository (our mapping)
7.3 Lack of capability or robustness taxonomy

Related incidents

Cite this record
StupidLLM. "STUPID-2026-0156: A Claude Code-written helper script's unquoted rm -rf, split apart by zsh, deleted a user's entire git repository including .git." AI Agent Incident Database, 2026. https://www.stupidllm.com/incident/STUPID-2026-0156/ (accessed 2026-11-05). CC BY 4.0.

← STUPID-2026-0155By ID

Get told when an agent breaks something

We document AI agent failures daily, severity-scored against a published scale. Each Monday, if any scored 7 or above that week — deleted data, leaked secrets, broken production — you get one email with the sources. When nothing did, you get nothing.

This database is callable over MCP — query it from inside your agent.