cursor

Block a command with a Claude Code PreToolUse hook

To stop Claude Code from running a specific Bash command, add a `PreToolUse` hook that matches the `Bash` tool, reads the command from its JSON input, and either returns `permissionDecision: "deny"` or exits with code 2 when the command matches. Per Claude Code's hooks docs, `PreToolUse` runs before the tool runs, and it's an event that can block the tool call. If the script exits 0 with no output, there's no decision and the normal permission flow applies. This post walks through that hook setup.

The setup

This follows the `block-rm` example in the hooks docs. Add the hook to `.claude/settings.json`:

json
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "if": "Bash(rm *)",
            "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/block-rm.sh"
          }
        ]
      }
    ]
  }
}

Then save the script to `.claude/hooks/block-rm.sh`:

bash
#!/bin/bash
# .claude/hooks/block-rm.sh
COMMAND=$(jq -r '.tool_input.command')

if echo "$COMMAND" | grep -q 'rm -rf'; then jq -n '{ hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "deny", permissionDecisionReason: "Destructive command blocked by hook" } }' else exit 0 # no decision; normal permission flow applies fi ```

Make it executable with `chmod +x .claude/hooks/block-rm.sh`. The script uses `jq`, so it needs to be on your PATH.

Exit code 2 or a deny decision

Exit code 2`permissionDecision: "deny"`
Blocks the callYesYes
HowExit with code 2Exit 0 and print JSON with `hookSpecificOutput`
Reason for ClaudeThe hook's stderr`permissionDecisionReason`
When to useA simple blockFiner control and an explicit reason

The docs describe JSON output as the finer-grained option: "Instead of exiting with code 2 to block, exit 0 and" return a decision.

Where to put it

From the docs' hook locations:

  • `~/.claude/settings.json`: all your projects, not shared.
  • `.claude/settings.json`: this project, can be committed so your team gets it.
  • `.claude/settings.local.json`: this project, gitignored.

Pitfalls

  • Script errors don't block. An exit code other than 0 or 2 with no JSON decision is a non-blocking error, so the command goes through.
  • Silence isn't approval. Exit 0 with no output only means the hook has no decision; the normal permission flow still runs.
  • Forgetting `chmod +x` or `jq`. The docs' example needs both.
  • Treating a pattern match as a guarantee. The hook checks the strings you tell it to check. Other events, failure modes and Cursor hooks are covered in Cursor hooks vs rules.

How to check it

Ask Claude Code to run a command your hook should block and confirm it's denied with your reason. Then run a command that shouldn't match and confirm it goes through the normal permission flow.

Where the kit fits

The Operator Pack ($47) ships markdown seats + skills + install.sh across Cursor, Claude Code, and Grok Bot. You can also start at /free. Cheat Sheet for the skim checklist mindset.

Soft next step

Pick the one command you never want run unasked, write a `PreToolUse` hook for it, and commit it in `.claude/settings.json`. Soft ladder: /free, then the Operator Pack ($47). Related: Cursor hooks vs rules, skill vs rule, CLAUDE.md and AGENTS.md symlink.

FAQ

How do I block a command in Claude Code?+

Add a PreToolUse hook that matches the Bash tool, checks the command, and returns a deny decision or exits with code 2 when it matches what you want to block, per Claude Code's hooks docs.

What is a PreToolUse hook?+

A hook that runs on every tool call in Claude Code's agent loop, before the tool runs. It's the event that can block a tool call.

Exit code 2 or permissionDecision deny: which should I use?+

Both block. Exit code 2 is the simple block. Returning JSON with `permissionDecision: "deny"` and a `permissionDecisionReason` gives finer control and a reason Claude can see.

Where do I put a Claude Code hook so my team gets it?+

In the project's `.claude/settings.json`, which can be committed. `~/.claude/settings.json` applies only to you, and `.claude/settings.local.json` is gitignored.

What does the hook receive as input?+

JSON on stdin describing the tool call. The docs' example reads the command from `.tool_input.command` with `jq`.

What happens if my hook script errors?+

An exit code other than 0 or 2, with no JSON decision, is a non-blocking error, so the call isn't blocked. Test the script on the commands you mean to block.

Get the free starter

Value first, then a kit when you are ready. No fake urgency. Start with Harness Desk Starter at /free.