Claude Code is useful because it can run tools, but that also means a project needs deterministic guardrails. Instructions in a prompt can be forgotten. A PreToolUse hook runs before the tool call and can reject it in code.
What we are building
This project hook inspects every Bash command. If it finds rm -rf, it writes a reason to stderr and exits with status 2. Claude Code blocks the command and receives the reason as feedback.
The important detail is the exit code:
- Exit
0: the hook has no objection; normal permissions still apply. - Exit
2: aPreToolUseaction is blocked. - Exit
1: this is only a non-blocking hook error for most events, so it is the wrong choice for a guardrail.
1. Add the hook configuration
Create .claude/settings.json in the project:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/block-dangerous.sh",
"args": []
}
]
}
]
}
}
The Bash matcher keeps this hook focused on shell commands. Using args: [] selects exec form, so the project path is passed without shell quoting problems.
2. Add the guard script
Create .claude/hooks/block-dangerous.sh:
#!/bin/bash
input=$(cat)
command=$(jq -r '.tool_input.command // empty' <<<"$input")
if grep -Eq '(^|[;&|[:space:]])rm[[:space:]]+-rf([[:space:]]|$)' <<<"$command"; then
echo "Blocked: rm -rf is not allowed in this project" >&2
exit 2
fi
exit 0
Then make it executable:
chmod +x .claude/hooks/block-dangerous.sh
The script requires jq. On macOS, install it with brew install jq.
3. Test it directly
You do not need to risk a real deletion to test the logic. Pipe sample hook input into the script:
printf '%s\n' '{"tool_input":{"command":"rm -rf /tmp/example"}}' | .claude/hooks/block-dangerous.sh
echo $?
The script should print the block reason and return 2. Test a safe command too:
printf '%s\n' '{"tool_input":{"command":"npm test"}}' | .claude/hooks/block-dangerous.sh
echo $?
That should return 0. In Claude Code, run /hooks to confirm the project hook is loaded.
The honest limitation
This is a focused example, not a complete shell parser. Real commands can hide destructive behavior inside scripts, aliases, interpreters, or unusual syntax. Add Claude Code permission deny rules for hard policy, keep backups, and run agents in a sandbox when the stakes are high. Hooks are an extra deterministic layer, not a replacement for least privilege.
Takeaway
Use prompts to explain preferred behavior. Use PreToolUse hooks for checks that must run every time. Most importantly, use exit code 2 when the hook needs to stop the action.
Source: Claude Code hooks reference.
Top comments (0)