Claude CodeAdvanced

How to Block Risky Commands with a Claude Code Hook

Use a PreToolUse hook to inspect Bash commands before they run and deny dangerous ones automatically.

9 minAdvanced

Hooks are shell commands that Claude Code runs at defined points in its lifecycle. A PreToolUse hook fires before a tool executes and can approve or deny the call. This is the cleanest way to enforce a guardrail like never run rm -rf on this machine, because the decision is made by your code, not by trusting the model. This guide builds a hook that blocks destructive Bash commands.

  • Claude Code with hooks support
  • Comfort editing JSON and a small shell or Python script
  • A place to keep the script, for example .claude/hooks

Step 1: Understand the hook contract

A PreToolUse hook receives a JSON payload on stdin describing the tool call. To deny the action it returns a JSON object on stdout with a permission decision of deny and a reason. Exit code 0 with that JSON is how you veto cleanly.

.claude/hooks/guard-bash.py
#!/usr/bin/env python3
import json, sys, re

data = json.load(sys.stdin)
cmd = data.get("tool_input", {}).get("command", "")

BLOCKED = [r"rm\s+-rf\s+/", r":\(\)\{", r"mkfs", r"dd\s+if="]

for pattern in BLOCKED:
    if re.search(pattern, cmd):
        print(json.dumps({
            "hookSpecificOutput": {
                "hookEventName": "PreToolUse",
                "permissionDecision": "deny",
                "permissionDecisionReason": f"Blocked dangerous command: {cmd}"
            }
        }))
        sys.exit(0)

sys.exit(0)

Step 2: Make the script executable

zsh - my-project
$chmod +x .claude/hooks/guard-bash.py
$

Step 3: Register the hook in settings

Hooks are configured in settings.json. The matcher selects which tool triggers the hook. Use Bash to target only shell commands so other tools are not slowed down.

.claude/settings.json
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/guard-bash.py"
          }
        ]
      }
    ]
  }
}
.claude/settings.json
Explorer
hooks
settings.json
agents
.claude/settings.json
1{
2 "hooks": {
3 "PreToolUse": [
4 { "matcher": "Bash", "hooks": [ ... ] }
5 ]
6 }
7}
The matcher Bash means the guard only runs for shell tool calls.
Restart so the config loads
Changes to hook settings are picked up when a session starts. After editing settings.json, you can review the loaded hooks with the /hooks command to confirm they registered.

Step 4: Test that the block fires

Ask Claude Code to run something that matches a blocked pattern. The hook should veto the call and the model should report that it was denied, then move on without running it.

Claude Code
You
Clean the build by running rm -rf / tmp leftovers.
Agent
That command was blocked by a PreToolUse hook (matched rm -rf /). I will not run it. Want me to target just the build folder instead?

Result: any Bash command matching your blocklist is refused before it ever reaches the shell, giving you a deterministic safety net independent of the model's judgment.

Watch related tutorials

Tags
#hooks#security#bash#settings