Claude Code Safety Guide: Prevent Accidental File Deletion with Hooks, Permissions & Git Worktrees
Why This Guide Exists
This guide documents Claude Code's documented track record of deleting files unintentionally. The incidents represent recurring failure modes when an agent has shell access.
Notable incidents include:
- October 21, 2025: Mike Wolak's home directory was wiped when Claude Code generated a destructive command with shell tilde expansion
- February 26, 2026: Claude Code executed
rm -rfagainst a Flutter project directory without authorization - April 24, 2026: A Cursor agent deleted an entire production database and backups in nine seconds
- Multiple GitHub issues documenting file destruction during routine operations
Anthropic released sandboxing on October 20, 2025, but it remained opt-in. Every layer in this guide requires explicit configuration—the defaults provide insufficient protection.
Layer 1: Permission Deny Rules in settings.json
Permission deny rules are evaluated first and override allow rules. They cannot be loosened by command-line flags or prompts.
Recommended baseline for .claude/settings.json:
{
"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/**)"
]
}
}
Pattern Matching Details
Word-boundary semantics: Bash(rm:*) requires rm followed by a space or end-of-string, matching rm -rf . but not rmdir. The form Bash(rm*) without the boundary would match rmdir and similar commands.
Process wrappers get stripped: Claude Code strips timeout, time, nice, nohup, stdbuf, and bare xargs before matching rules. Environment runners like devbox run and docker exec are not stripped.
Limitations
Pattern-based blocking cannot reliably catch:
- Variables:
DIR=~ && rm -rf $DIR - Subshells:
$(echo rm) -rf . - Compound chains where
rmis not the first command - Custom scripts calling
rminternally
Layer 2: A PreToolUse Hook That Inspects Every Command
A PreToolUse hook runs deterministic shell code on the full command string before execution. The model cannot override a blocking hook.
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 Hooks Catch What Deny Rules Miss
- Hooks see the literal command string, including subshells, pipes, and full
findinvocations - Exit code 0 with
denyJSON returns control to Claude with the reason attached - Hooks fire regardless of permission mode, even in
bypassPermissionsmode
Layer 3: Git Worktrees So Mistakes Are Recoverable
A git worktree gives the agent its own checkout on its own branch, so destructive runs affect only the worktree, not your main work.
Manual setup:
# From your main checkout on the feature branch
git worktree add ../myproject-agent agent/refactor-auth
cd ../myproject-agent
claude
If the agent deletes the entire working tree, your main copy remains intact. Clean up 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...
Layer 4: Replace rm With trash
Aliasing rm to a recoverable deletion tool turns permanent loss into recovery from trash.
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 users approve the rewritten command. Chain it after the block-destructive hook by registering both in the PreToolUse array.
Note: Putting alias rm='trash' in ~/.bashrc does not work for Claude Code, since the Bash tool spawns non-interactive shells. The hook approach is reliable.
Layer 5: Turn On the Sandbox
Anthropic's sandbox provides OS-level enforcement that prevents model confusion from bypassing protections. It restricts Bash and child processes to a defined filesystem and network boundary.
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, 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 /, home directory, or critical system paths still triggers a prompt as a circuit breaker.
The sandbox survives prompt injection—if a model gets convinced by hidden text to wipe your home directory, the sandbox hard-blocks the syscall.
Bonus: Disable bypassPermissions in Managed Settings
For team administration, lock out bypassPermissions at the managed-settings level in /etc/claude-code/managed-settings.json:
{
"permissions": {
"disableBypassPermissionsMode": "disable",
"disableAutoMode": "disable"
},
"allowManagedHooksOnly": true,
"allowManagedPermissionRulesOnly": true
}
allowManagedHooksOnly ensures only your security team's hooks are loaded—developers cannot turn off the block-destructive hook by editing .claude/settings.json.
The Recommended Setup
Layer everything. None is sufficient alone, and the cost of stacking them is one settings file and one shell script.
| Layer | What It Catches | What It Misses |
|---|---|---|
| Deny rules | Direct rm, sudo, force-push |
Compound commands, env runners, scripted deletions |
| PreToolUse hook | Anything you can regex against | Non-shell deletion (Edit tool overwriting a file) |
| Edit deny rules | Writes to .env, .git, secrets |
Symlinks pointing out of allowed dirs |
| Worktrees | Recoverable file destruction | Damage to repos outside the worktree |
| trash hook | Permanent file loss | Files outside trash-aware paths |
| Sandbox | OS-level filesystem and network boundary | Anything inside allowed paths |
Run it on a low-stakes project for a week and observe how often the hook fires. Most teams discover agents performed far more deletion than expected—they got lucky on the targets.
Every defense 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—read the complete guide to hooks, subagents, and skills. For provider setup and getting Claude Code talking to a custom endpoint, see the Claude Code configuration guide. For the underlying model and changes between Opus versions, see the Claude Opus API review. For comparisons between Claude Code, Codex CLI, Cursor, and DeepSeek TUI, the AI coding agents comparison covers the model layer underneath all of them.
If 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. The agent does not know the difference; your wallet might.
Originally published on ofox.ai/blog.
Top comments (0)