Claude Code Safety Guide: Prevent Accidental File Deletion with Hooks, Permissions & Git Worktrees

Claude Code Safety Guide: Prevent Accidental File Deletion with Hooks, Permissions & Git Worktrees

TL;DR — Claude Code can and does delete files unexpectedly. The fix is defense-in-depth: permission deny rules for the obvious patterns, a PreToolUse hook that inspects every Bash command, git worktrees so destructive runs are reversible, and Anthropic’s October 2025 sandbox for OS-level enforcement. Configure all four. Hope is not a strategy when the agent has a shell.

Mike Wolak watched Claude Code run `rm -rf ~/` against the instructions in his own CLAUDE.md and lose years of work in ten seconds — the lesson is that prompt-level rules are not a safety boundary, they are a suggestion.

Why This Guide Exists

This guide exists because Claude Code has a documented track record of deleting files it was not supposed to touch. The incidents are not edge cases. They are recurring failure modes when an agent has shell access and the user trusts the model’s judgment over deterministic guardrails.

  • October 21, 2025 — Mike Wolak’s home directory was wiped when Claude Code generated rm -rf tests/ patches/ plan/ ~/ and shell tilde expansion turned the trailing ~/ into his entire home, despite his global CLAUDE.md forbidding destructive commands. Tracked as #10077.
  • February 26, 2026 — A developer reported on GitHub issue #29082 that Claude Code executed rm -rf against a Flutter project directory (smart_drive_log) without authorization.
  • GitHub issue #30700 — Claude Code wiped a ~/Desktop directory including applications, with the command apparently navigating above the working directory before executing.
  • April 24, 2026 — A Cursor agent powered by Claude Opus 4.6 deleted PocketOS’s entire production database and backups in nine seconds, then admitted “I violated every principle I was given.”
  • GitHub issue #32637 — A Cowork session destroyed user files when reorganizing iCloud-offloaded documents, treating 0-byte placeholder stubs as real files during a cp followed by rm -rf.

Anthropic released sandboxing on October 20, 2025, one day before Wolak’s home directory was deleted. The sandbox was opt-in, and he did not opt in. That detail matters: every layer in this guide is opt-in. You have to configure them. The defaults will not save you.

If you are setting up Claude Code for the first time, start with our configuration guide and then come back here before pointing it at anything you cannot afford to lose.

Layer 1: Permission Deny Rules in settings.json

The cheapest fix is a deny rule in .claude/settings.json that blocks the obvious destructive patterns. Deny rules are evaluated first in Claude Code’s permission system — they override allow rules and ask rules, and they cannot be loosened by command-line flags or prompts.

Here is the baseline I recommend for any project:

{
  "permissions": {
    "deny": [
      "Bash(rm:*)",
      "Bash(sudo:*)",
      "Bash(chmod 777:*)",
      "Bash(git push --force:*)",
      "Bash(git push -f:*)",
      "Bash(git reset --hard:*)",
      "Bash(git clean:*)",
      "Bash(dd:*)",
      "Bash(mkfs:*)",
      "Bash(* > /dev/sda*)",
      "Read(~/.ssh/**)",
      "Read(**/.env)",
      "Edit(**/.env)",
      "Edit(.git/**)"
    ]
  }
}

Two things to understand about how these patterns actually match:

Word-boundary semantics. Bash(rm:*) is equivalent to Bash(rm *) — the trailing space requires rm followed by a space or end-of-string, so rm -rf . matches but rmdir does not. If you wrote Bash(rm*) without the boundary, it would also match rmdir and rmtemp — usually not what you want.

Process wrappers get stripped. Claude Code strips a fixed set of wrappers — timeout, time, nice, nohup, stdbuf, and bare xargs — before matching rules. So timeout 5 rm -rf . still hits your deny rule. But environment runners like devbox run, npx, and docker exec are not stripped. That means Bash(devbox run *) covers devbox run rm -rf . even though you would not want it to. Write specific allow rules for inner commands you trust, not blanket runner approvals.

The :* suffix is documented as equivalent to a trailing * and is the form the permission dialog writes when you choose “Yes, don’t ask again.” Use whichever reads better.

What Deny Rules Cannot Stop

Pattern-based blocking is fragile for command arguments. Anthropic’s own permissions docs call this out for URL filtering, and the same warning applies to file deletion. None of the following are reliably caught by Bash(rm:*):

  • Variables: DIR=~ && rm -rf $DIR
  • Subshells: $(echo rm) -rf .
  • Compound chains where rm is not the first command: a find . -name "*.tmp" -delete invocation never sees rm in the command string
  • Custom scripts: ./scripts/cleanup.sh that calls rm internally

For those, you need Layer 2.

Layer 2: A PreToolUse Hook That Inspects Every Command

A PreToolUse hook is the strongest single safety layer because it runs deterministic shell code on the full command string before Claude Code executes anything. The model cannot talk it out of denying. A blocking hook takes precedence over allow rules.

Create .claude/hooks/block-destructive.sh:

#!/bin/bash
# Read the full Bash invocation from stdin
CMD=$(jq -r '.tool_input.command')

# Patterns that should never run unattended
DANGEROUS='(^|[;&|`$(]| )(rm[[:space:]]+-[a-z]*[rRfF]|sudo[[:space:]]|chmod[[:space:]]+777|find[[:space:]].+-delete|find[[:space:]].+-exec[[:space:]]+rm)'

if echo "$CMD" | grep -Eq "$DANGEROUS"; then
  jq -n --arg cmd "$CMD" '{
    hookSpecificOutput: {
      hookEventName: "PreToolUse",
      permissionDecision: "deny",
      permissionDecisionReason: ("Blocked by safety hook: " + $cmd)
    }
  }'
  exit 0
fi

exit 0

Make it executable: chmod +x .claude/hooks/block-destructive.sh.

Wire it into .claude/settings.json:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/block-destructive.sh"
          }
        ]
      }
    ]
  }
}

Why this catches what deny rules miss:

  • The hook sees the literal command string, including subshells, pipes, and the full find invocation — your regex can match -delete or -exec rm, both of which slip past Bash(rm:*).
  • Exit code 0 with the deny JSON returns control to Claude with the reason attached, so it explains why and tries a different approach. Exit code 2 also blocks but is rougher — it surfaces stderr to the model without structured context.
  • Hooks run regardless of permission mode. Even in bypassPermissions mode, your hook still fires.

For a deeper walkthrough of the hook system — including the other 24 lifecycle events, subagent scoping, and skill integration — see our Claude Code hooks, subagents, and skills guide.

Layer 3: Git Worktrees So Mistakes Are Recoverable

A git worktree gives the agent its own checkout on its own branch, so a destructive run blows away the worktree, not your real work. Worktree isolation has moved from a power-user trick to a built-in feature in Claude Code subagents — you can put isolation: worktree in a subagent’s frontmatter and Anthropic spins up the worktree automatically.

Manual setup, if you want to wrap any agent run:

# From your main checkout on the feature branch
git worktree add ../myproject-agent agent/refactor-auth
cd ../myproject-agent
claude

Now the agent operates in ../myproject-agent. If it deletes the entire working tree, your main copy at ../myproject is intact. Clean up the worktree when done:

cd ../myproject
git worktree remove ../myproject-agent
git branch -D agent/refactor-auth  # optional

For subagents, declare worktree isolation in the agent definition (e.g., .claude/agents/refactorer.md):

---
name: refactorer
description: Performs large refactors in an isolated worktree
tools: Read, Edit, Write, Bash
isolation: worktree
---

You are a refactoring specialist. Make incremental changes...

The combination that actually works in practice: deny rules + hook in the parent shell, worktrees for any agent run touching more than a single file. Reversibility is cheap once you set it up. Recovery from rm -rf ~/ is not.

Layer 4: Replace rm With trash

Aliasing rm to a recoverable deletion tool turns “permanent loss” into “fish it out of the trash.” This is the lowest-effort layer with the highest payoff: even when every other defense fails, you have an undo button.

On macOS:

brew install trash

Then in .claude/hooks/coerce-rm.sh:

#!/bin/bash
CMD=$(jq -r '.tool_input.command')

# If the command uses bare rm (not /bin/rm, not safe-rm), rewrite to trash
if echo "$CMD" | grep -Eq '(^|[;&|`$(]| )rm[[:space:]]+'; then
  NEW=$(echo "$CMD" | sed -E 's/(^|[;&|`$(]| )rm[[:space:]]+/\1trash /g')
  jq -n --arg cmd "$NEW" '{
    hookSpecificOutput: {
      hookEventName: "PreToolUse",
      permissionDecision: "ask",
      permissionDecisionReason: ("Rewriting rm to trash. Approve to run: " + $cmd)
    }
  }'
  exit 0
fi
exit 0

This hook returns permissionDecision: "ask" so the user gets the chance to approve the rewritten command. You can chain it after the block-destructive hook by registering both in the PreToolUse array — Claude Code runs them in order, and the first hook that returns a decision other than defer wins. (The community plugin safe-rm-claude-plugin implements a similar pattern with finer-grained protection for .env, git-tracked code, and system files.)

A note on the standard alias trick: putting alias rm='trash' in ~/.bashrc does not work for Claude Code, because the Bash tool spawns non-interactive shells that skip .bashrc. The hook approach is the reliable one.

Layer 5: Turn On the Sandbox

Anthropic’s sandbox is OS-level enforcement that no amount of model confusion can bypass. It restricts Bash and its child processes to a defined filesystem and network boundary, and the sandbox bytes are merged with your Read and Edit deny rules.

Enable it in .claude/settings.json:

{
  "sandbox": {
    "enabled": true,
    "filesystem": {
      "allowRead": ["."],
      "denyRead": ["~/.ssh", "~/.aws", "**/.env"],
      "allowWrite": ["~/.npm", "~/.cache"]
    },
    "network": {
      "allowedDomains": ["registry.npmjs.org", "api.github.com"]
    },
    "autoAllowBashIfSandboxed": true
  }
}

With autoAllowBashIfSandboxed: true (the default), sandboxed Bash runs without permission prompts because the OS boundary substitutes for per-command approval. Explicit deny rules still apply, and rm or rmdir against /, your home directory, or other critical system paths still triggers a prompt as a circuit breaker — even sandboxed, Anthropic refuses to silently allow rm -rf /.

The sandbox is the layer that survives prompt injection. If a model gets convinced by hidden text in a markdown file to wipe your home directory, deny rules and hooks may still hold, but the sandbox is the only layer that hard-blocks the syscall.

Bonus: Disable bypassPermissions in Managed Settings

If you administer Claude Code for a team, lock out bypassPermissions at the managed-settings level so no developer can run with safeties off. In /etc/claude-code/managed-settings.json (Linux) or the equivalent path on your platform:

{
  "permissions": {
    "disableBypassPermissionsMode": "disable",
    "disableAutoMode": "disable"
  },
  "allowManagedHooksOnly": true,
  "allowManagedPermissionRulesOnly": true
}

allowManagedHooksOnly ensures that only your security team’s hooks are loaded — a curious developer cannot turn off the block-destructive hook by editing their own .claude/settings.json. Pair this with a managed deny ruleset for rm -rf, sudo, and the destructive git commands.

Layer everything. None of these is sufficient alone, and the cost of stacking them is one settings file and one shell script.

LayerWhat It CatchesWhat It Misses
Deny rulesDirect rm, sudo, force-pushCompound commands, env runners, scripted deletions
PreToolUse hookAnything you can regex againstNon-shell deletion (Edit tool overwriting a file)
Edit deny rulesWrites to .env, .git, secretsSymlinks pointing out of allowed dirs (use Read(~/.ssh/**) to deny target too)
WorktreesRecoverable file destructionDamage to repos outside the worktree
trash hookPermanent file lossFiles outside the trash-aware paths
SandboxOS-level filesystem and network boundaryAnything inside the allowed paths

Run it on a low-stakes project for a week and watch how often the hook fires. Most teams discover their agents were doing far more deletion than expected — they just got lucky on the targets.

Every defense in this guide is opt-in, every default is loose, and every Claude Code horror story starts with someone trusting the model to remember a rule it was never enforced to follow.

Where to Go Next

For more on Claude Code’s extensibility surface — hooks, subagents, and skills as a coherent system — read our complete guide to hooks, subagents, and skills. For provider setup and getting Claude Code talking to a custom endpoint, see our Claude Code configuration guide. For the underlying model and what changed between Opus 4.6 and 4.7, see the Claude Opus 4.7 API review. And if you are still picking between Claude Code, Codex CLI, Cursor, and DeepSeek TUI, the AI coding agents comparison covers the model layer that sits underneath all of them.

If you are running Claude Code against a custom Anthropic-compatible endpoint, ofox.ai supports the full Anthropic protocol at https://api.ofox.ai/anthropic — including extended thinking and cache_control. Same permission system, same hooks, same safety layers. The agent does not know the difference; your wallet might.


Sources for incident references: Live Science on the PocketOS database deletion, Tom’s Hardware on the Cursor/Claude rogue agent, GitHub issues #10077, #29082, #30700, and #32637 in the anthropics/claude-code repo. Permission and hook syntax verified against code.claude.com/docs/en/permissions and Claude Code sandboxing announcement (October 20, 2025).