Hook Exit Codes

The complete guide to what exit codes mean in Claude Code hooks. Includes the bash syntax error trap that can lock your entire session.

The Three Exit Codes

Exit CodeMeaningEffect
0Allow / PassthroughTool call proceeds normally. Default for hooks that don't match.
1Hook Error (ignored)Hook crashed or failed. Tool call still proceeds. Error shown in UI.
2BlockTool call is prevented. Claude sees "blocked by hook" message.

stdout JSON Decisions

Hooks can also output JSON to stdout for more control:

stdout JSONEffect
{"decision":"approve","reason":"Safe command"}Auto-approve (skip permission prompt)
{"decision":"block","reason":"Too dangerous"}Block the tool call
Priority: Exit code 2 always blocks, regardless of stdout. stdout JSON is checked only when exit code is 0.

The Bash Syntax Error Trap

DANGER: Bash syntax errors return exit code 2 — the same code that means "block." A broken hook script will accidentally block tool calls.

Example of the trap:

#!/bin/bash
INPUT=$(cat)
if [ -z "$INPUT" ]    # Missing 'then'
fi                     # bash: syntax error near unexpected token 'fi'
exit 0

This script never reaches exit 0. Bash aborts at the syntax error with exit code 2. Claude Code sees exit 2 and blocks the tool call.

If the hook's matcher is "" (all tools), this blocks Read, Write, Edit, Bash, Grep, and Agent — making the session completely unrecoverable.

How to Prevent It

# Always syntax-check before deploying a hook:
bash -n ~/.claude/hooks/my-hook.sh

# Test with empty input:
echo '{}' | bash ~/.claude/hooks/my-hook.sh
echo $?   # Must be 0, not 2

# Or use the automated validator:
npx cc-safe-setup --validate
Best practice: Never use matcher "" during development. Use "Bash" to limit blast radius if something goes wrong.

Recovery from a Broken Hook

SituationSolution
Hook crashes (exit 1)No action needed — tool call proceeds. Fix the hook when convenient.
Hook blocks (exit 2, intentional)Working as designed. The hook prevented a dangerous operation.
Hook blocks (exit 2, syntax error)From outside Claude Code: rm ~/.claude/hooks/broken-hook.sh
All tools blocked (matcher "")From outside: rm ~/.claude/hooks/broken-hook.sh then restart Claude Code
Can't identify the broken hooknpx cc-safe-setup --validate (auto-detects and disables)
Emergency: disable everythingnpx cc-safe-setup --safe-mode

Hook Template (Safe)

#!/bin/bash
# Safe hook template — always exits 0 on empty/unknown input
INPUT=$(cat)
TOOL=$(echo "$INPUT" | jq -r '.tool_name // empty' 2>/dev/null)
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)

# Only process relevant tools
[[ "$TOOL" != "Bash" ]] && exit 0
[ -z "$COMMAND" ] && exit 0

# Your logic here
# if dangerous: exit 2
# if safe: echo '{"decision":"approve","reason":"..."}' && exit 0
# otherwise: exit 0

exit 0