Hooks are not just for convenience. A PreToolUse hook can stop an action before it happens, which makes it a real safety net. The trick is the exit code your hook returns.
How blocking works
When a PreToolUse hook exits with code 2, that is a blocking error. Claude Code stops the tool call and feeds the hook's standard error back to Claude as feedback. Exit code 0 means success, and any other non-zero code is treated as a non-blocking error, meaning the action still proceeds.
So to block something, exit 2 and print a reason to stderr.
A guard script
Wire a script into the Bash tool:
{
"hooks": {
"PreToolUse": [
{ "matcher": "Bash",
"hooks": [{ "type": "command", "command": "./.claude/guard.sh" }] }
]
}
}
The script reads the event from stdin, pulls out the command, and checks it:
#!/usr/bin/env bash
cmd=$(jq -r '.tool_input.command')
if echo "$cmd" | grep -qE 'rm -rf /|git push --force'; then
echo "Blocked: this command is not allowed." >&2
exit 2
fi
exit 0
What Claude sees
Because the message went to stderr and the script exited 2, Claude Code refuses to run the command and hands your message back to Claude. Claude reads "Blocked: this command is not allowed" and adjusts, rather than silently hitting a wall.
Keep the list tight
Start with a short list of commands you truly never want run, like force pushes or recursive deletes at the root. A guard that blocks too much becomes noise you will end up disabling. A small, sharp list stays useful and builds trust that your dangerous-command net actually holds.
Comments
Be the first to comment.