How to Block Dangerous Commands with Claude Code Hooks

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: a PreToolUse action 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 '%sn' '{"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 '%sn' '{"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.

Total
0
Shares
Leave a Reply

Your email address will not be published. Required fields are marked *

Previous Post

QA Documentation Gaps: The Audit Risk in UK Regulatory Reviews

Next Post

LUCID Compact 5.0 MP Triton GigE Camera

Related Posts