Block dangerous commands in Claude Code with a PreToolUse hook (tested)
We actually blocked `rm -rf` with a PreToolUse hook and verified the output.
What we tested
We verified, on this Mac, that a `PreToolUse` hook can actually stop a dangerous command before it runs. If the hook script exits with code 2, the tool call itself is blocked.
settings.json
Register a `PreToolUse` hook scoped to the Bash tool via `matcher`.
{
"hooks": {
"PreToolUse": [
{ "matcher": "Bash", "hooks": [ { "type": "command", "command": "bash .claude/hooks/block-rm.sh" } ] }
]
}
}The hook script
The hook receives JSON on stdin (`tool_name`, `tool_input`, ...). Extract `tool_input.command`, match destructive patterns, exit 2 to block or 0 to allow.
#!/bin/bash
input=$(cat)
cmd=$(echo "$input" | python3 -c "import json,sys; print(json.load(sys.stdin).get('tool_input',{}).get('command',''))")
if echo "$cmd" | grep -Eq 'rm[[:space:]]+-[a-zA-Z]*r[a-zA-Z]*f'; then
echo "BLOCKED" >&2; exit 2
fi
exit 0Real result
Running `claude -p "...rm -rf /tmp/should_not_exist_xyz123..."` non-interactively, the command never ran. The actual output was: `PreToolUse:Bash hook error: [...]: BLOCKED: destructive rm -rf command is not allowed by policy.` — and Claude itself confirmed the hook worked as configured.
The same pattern stops force-push, too
The same PreToolUse pattern can catch `git push --force`, production hostnames, or writes to `.env`. The point: a deterministic exit code, not a markdown reminder that can be ignored.
Caveat
Grep-based matching can be worked around. For truly critical actions, pair hooks with `permissions.deny` and human review.