Part of the AI coding agent security checklist
How to block a command in Claude Code with a PreToolUse hook
To block a command in Claude Code, register a PreToolUse hook with the matcher Bash. Before each Bash call, Claude Code sends the hook JSON on stdin, with the command in tool_input.command. If the hook exits with code 2, the call is cancelled and whatever the hook wrote to stderr is what Claude sees as the reason. Below: a tested script that blocks rm -rf and curl | sh, the settings that register it, and the pitfalls.
Checked against Claude Code's hooks reference and hooks guide under Sources; the script was run against the inputs shown. Published .
The hook script
Save this as ~/.claude/hooks/block-commands.py. It reads the hook input, checks the command against two patterns, and exits 2 with a reason when one matches. Anything else exits 0, which leaves the call to Claude Code's normal permission flow.
#!/usr/bin/env python3
import json, re, sys
try:
command = json.load(sys.stdin)["tool_input"]["command"]
except Exception:
print("block-commands.py could not read the command", file=sys.stderr)
sys.exit(2)
BLOCKED = [
(r"\brm\s+(-\S*\s+)*-\S*[rR]", "recursive rm"),
(r"\b(curl|wget)\b[^|]*\|\s*(sudo\s+)?(ba|z)?sh\b", "a download piped into a shell"),
]
for pattern, name in BLOCKED:
if re.search(pattern, command):
print("Blocked by block-commands.py: " + name + ". Ask the user to run it.",
file=sys.stderr)
sys.exit(2)
sys.exit(0)Register it in settings.json
Add the hook under PreToolUse in ~/.claude/settings.json for every project on this machine. If the file already has a hooks key, merge into it.
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "python3 \"$HOME/.claude/hooks/block-commands.py\""
}
]
}
]
}
}Test it with a harmless command
Run the script by hand first, with the same JSON Claude Code would send, and check the exit status.
echo '{"tool_name":"Bash","tool_input":{"command":"rm -rf build"}}' \
| python3 ~/.claude/hooks/block-commands.py; echo "exit $?"
# Blocked by block-commands.py: recursive rm. Ask the user to run it.
# exit 2
echo '{"tool_name":"Bash","tool_input":{"command":"npm test"}}' \
| python3 ~/.claude/hooks/block-commands.py; echo "exit $?"
# exit 0Then try it in a session on something you would not miss. Make an empty folder with mkdir /tmp/hook-test and ask Claude to run rm -rf /tmp/hook-test. The call should be refused with your reason, and the folder should still be there. If it is gone, the hook did not run: check /hooks and the matcher, and nothing of value was lost.
The alternative: a JSON deny decision
Instead of exiting 2, a PreToolUse hook can exit 0 and print a decision on stdout. Claude Code shows the permissionDecisionReason of a deny to Claude, as it shows stderr for exit 2.
{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"permissionDecisionReason": "Recursive rm is blocked. Ask the user to run it."
}
}Pitfalls
Where ranwhat fits
A hook works from the moment you add it. To see whether a recursive delete or a downloaded script piped into a shell already ran, uvx ranwhat check reads Claude Code's transcripts on your machine, flags those calls among others with watch's rules, and changes nothing. If you would rather not maintain patterns, ranwhat hook install adds a PreToolUse hook of its own: a call those rules rate high or critical waits for your yes, or with --mode deny a critical one is refused. It judges on your machine, sends nothing, fails open like any hook, and needs a lasting install rather than uvx.
Next
For deny rules, Read guards and what hooks cannot stop, read Claude Code hooks for security. For every other control around a coding agent, the AI coding agent security checklist. The rest of the guides are listed here.
Sources
- Hooks referencePreToolUse input, exit codes, permissionDecision, CLAUDE_PROJECT_DIR, security
- Hooks guideblocking with exit 2, executable scripts, case-sensitive matchers, testing by hand
- Configure permissionsdeny rules and what a Bash rule does not match
- ranwhat referencewhat ranwhat hook install does and does not do