Each one has caused real incidents. Each one has a simple fix.
Bash returns exit code 2 for syntax errors. Claude Code treats exit 2 as "block." A broken script silently blocks every tool call.
# This script has a syntax error (missing 'then'):
if [ -z "$INPUT" ]
fi
# bash exits with code 2 → Claude blocks the tool
bash -n hook.sh before deploying. Or: npx cc-safe-setup --validateMatcher "" applies to ALL tools (Bash, Read, Write, Edit, Grep, Agent). If your hook breaks, you can't even read files to debug it.
// DON'T:
{"matcher": "", "hooks": [...]}
// DO:
{"matcher": "Bash", "hooks": [...]}
"Bash" during development. Only use "" for well-tested production hooks.Most hooks use jq to parse JSON stdin. If jq isn't installed, the hook silently fails and does nothing.
# This silently fails without jq:
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command')
npx cc-safe-setup --doctor — it checks jq installation first.If the last command in your script fails, bash inherits that exit code. If it happens to be 2, you accidentally block.
#!/bin/bash
INPUT=$(cat)
grep -q "something" <<< "$INPUT" # returns 1 if no match
# Script exits with grep's exit code (1) — hook error
exit 0Matching rm in the command string catches directory names like soft-hold-enrollment that contain "rm".
# DON'T: matches "rm" in directory names
grep -q "rm" <<< "$COMMAND"
# DO: match rm as a word boundary with flags
grep -qE '\brm\s+-[rf]' <<< "$COMMAND"
\b word boundaries and \s+ whitespace in regex patterns.Hooks receive {} for some edge cases. If your script doesn't handle empty fields, it may crash or block unexpectedly.
# Safe pattern:
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
[ -z "$COMMAND" ] && exit 0 # exit early on empty
Hooks have a timeout. If your hook does network calls or heavy processing, it may time out and silently fail. On Windows, Python hooks take 200-500ms to start.
npx cc-safe-setup --benchmark to measure.