Hooks are shell scripts (or any executable) that Claude Code invokes at specific points in its tool loop. They allow you to intercept, block, log, or transform tool calls without modifying Claude's prompt.
# Event | When it fires | Can block? | Payload fields # --------------|----------------------------|------------|---------------- # PreToolUse | Before tool execution | YES (exit 2)| tool_name, tool_input, session_id # PostToolUse | After tool execution | No | tool_name, tool_input, tool_output, session_id # Notification | On user-visible alert | No | message, level, session_id # Stop | On session exit | No | exit_reason, session_id
Exit code 2 only blocks on PreToolUse. On PostToolUse, Stop, and Notification, exit code 2 is treated as a generic error — it does NOT undo the tool call or halt the session.
{
"event": "PreToolUse",
"session_id": "sess_abc123",
"tool_name": "Write",
"tool_input": {
"file_path": "/src/auth.ts",
"content": "..."
}
}
{
"event": "PostToolUse",
"session_id": "sess_abc123",
"tool_name": "Bash",
"tool_input": { "command": "npm test" },
"tool_output": { "stdout": "...", "stderr": "", "exit_code": 0 }
}
{
"event": "Notification",
"session_id": "sess_abc123",
"message": "Claude Code wants to run: rm -rf dist/",
"level": "warn"
}
{
"event": "Stop",
"session_id": "sess_abc123",
"exit_reason": "end_turn"
}
In settings.json, hooks are registered with matcher conditions that limit which events trigger them.
{
"hooks": [
{
"event": "PreToolUse",
"matcher": {
"tool_name": "Bash"
},
"command": "/usr/local/bin/bash-hook.sh"
},
{
"event": "PreToolUse",
"matcher": {
"tool_name": "Write",
"tool_input.file_path": "migrations/**"
},
"command": "/usr/local/bin/migration-guard.sh"
}
]
}
A PreToolUse hook can modify the tool input by printing a replacement JSON object to stdout before exiting 0.
#!/usr/bin/env bash # rewrite-paths.sh — redirect all writes to /tmp/sandbox set -euo pipefail input=$(cat) # read full JSON payload from stdin tool=$(echo "$input" | jq -r '.tool_name') if [ "$tool" = "Write" ]; then # Rewrite file_path to /tmp/sandbox prefix echo "$input" | jq '.tool_input.file_path |= "/tmp/sandbox" + .' exit 0 fi # No transform for other tools — output nothing, exit 0 exit 0
Use `jq -r '.tool_name'` (not echo + grep) to extract fields from the payload. This handles special characters in file paths and command strings correctly.