All articles

Blocking Dangerous Commands with Hooks

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.