Hooks are shell commands or LLM prompts that run automatically in response to Claude Code lifecycle events. They are configured in .claude/settings.json.
When: Session starts, resumes from idle, or context is compressed.
Can block: No
Best for: Injecting project status, displaying a context banner.
{
"SessionStart": [
{
"hooks": [
{
"type": "command",
"command": "echo \"MyProject | Branch: $(git branch --show-current) | Uncommitted: $(git status --porcelain | wc -l) files\""
}
]
}
]
}Matcher: You can optionally add a "matcher": "compact" to run only on context compaction events rather than every session start.
When: Before Claude executes a tool (Edit, Write, Bash, etc.)
Can block: Yes — exit code 2 blocks the tool execution.
stdin: Receives JSON with the tool call details:
{
"tool_name": "Edit",
"tool_input": {
"file_path": "/path/to/file.ts",
"old_string": "...",
"new_string": "..."
}
}Best for: Protecting sensitive files, blocking dangerous operations.
{
"PreToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "jq -r '.tool_input.file_path // empty' | grep -q '\\.env$' && echo 'BLOCKED: .env protected' >&2 && exit 2 || true"
}
]
}
]
}Matcher patterns: Use | to match multiple tools: "Edit|Write", "Bash", "Read|Glob".
When: After Claude executes a tool successfully.
Can block: No
stdin: Same JSON format as PreToolUse.
Best for: Auto-compilation, auto-linting, logging.
{
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "FILE=$(jq -r '.tool_input.file_path // empty') && case \"$FILE\" in *.ts|*.tsx) ./node_modules/.bin/tsc --noEmit --pretty 2>&1 | tail -20 ;; *.py) mypy . 2>&1 | tail -20 ;; esac; exit 0"
}
]
}
]
}When: Claude finishes its response (about to go idle).
Can block: Yes — exit code 2 blocks and forces Claude to continue.
Best for: Uncommitted changes reminders, quality gates.
{
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "CHANGED=$(git diff --name-only 2>/dev/null | wc -l | tr -d ' ') && [ \"$CHANGED\" -gt 0 ] && echo \"Reminder: ${CHANGED} files modified but not committed\" || true"
}
]
}
]
}When: Claude marks a task as completed.
Can block: Yes — exit code 2 blocks task completion.
Best for: Enforcing compilation passes before marking work as done.
{
"TaskCompleted": [
{
"hooks": [
{
"type": "command",
"command": "./node_modules/.bin/tsc --noEmit --pretty 2>&1 | tail -10 && echo 'Type check passed' || (echo 'BLOCKED: Type check failed' >&2 && exit 2)"
}
]
}
]
}When: Claude needs user attention (e.g., waiting for input).
Can block: No
Best for: Desktop notifications, Slack alerts.
When: A monitored file changes on disk.
Can block: No
Best for: Hot reload, file synchronization.
| Type | Description | Use Case |
|---|---|---|
command |
Execute a shell command | File protection, compilation, linting |
http |
POST to a URL | Slack/Teams notifications, external CI triggers |
prompt |
Single-turn LLM judgment | Quick yes/no decisions |
agent |
Multi-step sub-agent verification | Complex code review, test validation |
The most common type. Runs a shell command and captures stdout/stderr.
{
"type": "command",
"command": "echo 'Hello from hook'"
}- stdout is shown to Claude as context
- stderr is shown as warnings/errors
- exit 0 = success
- exit 2 = block the operation (PreToolUse, Stop, TaskCompleted only)
- Other exit codes = treated as errors but don't block
Sends a POST request to a URL.
{
"type": "http",
"url": "https://hooks.slack.com/services/YOUR/WEBHOOK/URL",
"headers": { "Content-Type": "application/json" },
"body": "{\"text\": \"Claude Code session started\"}"
}Runs a single LLM turn for quick judgment.
{
"type": "prompt",
"prompt": "Check if this change could break existing functionality. Return JSON: {\"decision\": \"allow\"} or {\"decision\": \"block\", \"reason\": \"...\"}"
}Spawns a sub-agent for multi-step verification.
{
"type": "agent",
"prompt": "Run the full test suite and report any failures.",
"model": "sonnet"
}-
Always end command hooks with
; exit 0if you don't want non-zero exit codes from intermediate commands to accidentally block operations. -
Use
tail -Nto limit output from verbose commands (tsc, mypy) — too much output floods Claude's context. -
On Windows,
npxmay not resolve local binaries correctly. Use./node_modules/.bin/tscinstead ofnpx tsc. -
Matcher is optional — omit it to run the hook for all tool calls of that event type.
-
Multiple hooks per event — you can have multiple hook entries for the same event. They run in order.