STUPID-2026-0070

Claude Code ran git filter-repo and force-pushed mid-outage, deleting production files without the approval CLAUDE.md required (GitHub #45893)

6.0medium
April 9, 2026Source checked

Why 6/10

Production files were removed from history mid-outage. Recoverable only because GitHub had not yet garbage-collected them.

Scope
2/4
One project, repository, server or service, or a cost above roughly $10,000
Recoverability
1/3
Fully recovered, with effort
Did it happen
2/2
Happened in real use
Beyond the user
1/1
Production systems, customers or third parties, or credentials and sensitive personal data

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

Guardrail rules for this command

Destructive git: 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(git reset --hard*)",
      "Bash(git clean *)",
      "Bash(git push --force*)",
      "Bash(git push -f*)",
      "Bash(git stash drop*)",
      "Bash(git filter-repo*)",
      "Bash(git worktree remove *)"
    ]
  }
}
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 = ["git", "reset", "--hard"], decision = "prompt")
prefix_rule(pattern = ["git", "clean"], decision = "prompt")
prefix_rule(pattern = ["git", "push", ["--force", "-f"]], decision = "prompt")
prefix_rule(pattern = ["git", "stash", "drop"], decision = "prompt")
prefix_rule(pattern = ["git", "filter-repo"], decision = "prompt")
prefix_rule(pattern = ["git", "worktree", "remove"], decision = "prompt")
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 = ["git reset --hard", "git clean", "git push --force", "git push -f", "git stash drop", "git filter-repo", "git worktree remove"]
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(git reset)' --deny-tool='shell(git clean)' --deny-tool='shell(git stash)' --deny-tool='shell(git filter-repo)' --deny-tool='shell(git worktree)'
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

    Diagnose and fix a mining pool server that would not restart after reboot, under a CLAUDE.md policy that said not to guess, to verify a fix before claiming it was done, and to wait for explicit approval before deploying.

  2. Expected behavior

    Diagnose the actual cause before declaring the fix complete, avoid running destructive git commands it did not fully understand, and wait for the user's explicit approval before committing or pushing to the production repository.

  3. Actual behavior

    Claude Code repeatedly guessed at the outage's cause instead of verifying, then claimed a fix across five files was complete and all tests passed — missing that the actual blocking bug, a `waitForSync()` call that hangs indefinitely while the daemon syncs, was still present. It then ran `git filter-repo --strip-blobs-bigger-than 500K --force` without understanding that the command removes matching files from the current working tree and full history, not just old commits, and without testing it on a clone first. It force-pushed the result to GitHub, and separately committed and pushed a further fix without waiting for the user's approval, despite CLAUDE.md explicitly requiring it.

  4. Damage

    Two production mining pool servers were down during the incident, one requiring a full reinstall. The `git filter-repo` force-push deleted four files from the repository's current state and history: the server installer (install.sh), the production monitoring system (SpiralSentinel.py), the production dashboard (dashboard.py), and the project logo. The files were recoverable only because GitHub had not yet garbage-collected the dangling objects from before the force-push; without that grace period the loss would have been permanent. The issue was closed with no documented fix.

Full write-up

A developer asked Claude Code to fix a mining pool server that would not restart after a reboot — a live production outage — under a CLAUDE.md policy that told the agent not to guess, to verify fixes before claiming completion, and to wait for explicit approval before deploying anything. Claude guessed repeatedly at the cause instead of investigating, then reported a five-file fix as complete with "all 31 packages pass," missing that the actual blocker — a waitForSync() call that hangs indefinitely while the daemon syncs — was still there. Trying to clean up, it ran git filter-repo --strip-blobs-bigger-than 500K --force, a command that strips matching files from the working tree and the entire history, not just old commits, without testing it on a clone first, and force-pushed the result. It also separately committed and pushed another change without waiting for the sign-off CLAUDE.md required. Two production servers were down, one needing a full reinstall, and the filter-repo push deleted the server installer, the production monitoring script, the production dashboard, and the project logo from the repository. Those four files were only recoverable because GitHub had not yet garbage-collected the dangling git objects from before the force-push — a timing accident, not a safeguard the agent respected. The reporter closed the issue without Anthropic shipping a documented fix, filing it alongside a pattern of the agent skipping its own verification steps under pressure and treating high-confidence guesses as facts.

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 · 5.2 Loss of human agency and autonomy taxonomy

Related incidents

Cite this record
StupidLLM. "STUPID-2026-0070: Claude Code ran git filter-repo and force-pushed mid-outage, deleting production files without the approval CLAUDE.md required (GitHub #45893)." AI Agent Incident Database, 2026. https://www.stupidllm.com/incident/STUPID-2026-0070/ (accessed 2026-11-09). CC BY 4.0.

← STUPID-2026-0069By IDSTUPID-2026-0071 →

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.