Hooks are shell commands Claude Code runs at specific lifecycle events. Each hook receives the event payload as JSON on stdin and signals back via exit code:
0— allow / continue normally2— block; stderr is surfaced to the model- anything else — advisory, logged
See the official hooks reference.
| Event | When | Can block? | Available since |
|---|---|---|---|
SessionStart |
New session opens | no | base |
UserPromptSubmit |
User sends a prompt | yes (rare) | base |
PreToolUse |
Before Claude calls a tool | yes | base |
PostToolUse |
After Claude calls a tool | no, but can mutate output | base; updatedToolOutput in v2.1.122 |
PostToolUseFailure |
After a tool call errored | no | recent |
PreCompact |
Before context is compacted | yes | v2.1.105 |
PermissionDenied |
Auto-mode classifier denied a call | no | v2.1.89 |
TaskCreated |
A Task was created via TaskCreate | no | v2.1.84 |
Notification |
System notification | no | base |
Stop |
Session ends | no | base |
See scripts/hooks/format-on-save.sh.
Wired to PostToolUse with matcher Edit|Write|MultiEdit. Runs Prettier /
Ruff / gofmt / rustfmt based on file extension.
See scripts/hooks/block-dangerous-bash.sh.
Wired to PreToolUse with matcher Bash. Returns exit 2 for patterns
like rm -rf /, force-push to main, pipe-to-shell from the internet.
See scripts/hooks/inject-context.sh.
Wired to SessionStart with matcher startup. Prints branch, recent
commits, dirty file count, open PRs to stdout — injected into the session.
See scripts/hooks/session-cost.sh.
Wired to Stop. Reads cost.total_cost_usd and num_turns from stdin,
prints a one-liner to stderr. As of v2.1.121, PostToolUse payloads
include duration_ms; the script now tails logs/PostToolUse-events.jsonl
to add a "top tools by time" breakdown.
See scripts/hooks/pre-compact.sh.
Wired to PreCompact (v2.1.105+). Writes .claude/checkpoints/pre-compact-*.md
with branch, dirty file count, and last commit. Permissive by default
(always exits 0). Customize the block-conditions if you want compaction
gated on, e.g., "no in-progress refactor without tests".
See scripts/hooks/redact-secrets.sh.
Wired to PostToolUse matcher Bash|Read|Grep. Demonstrates the
v2.1.122 hookSpecificOutput.updatedToolOutput field — the hook rewrites
the tool result the model sees, replacing secret-shaped strings (API
keys, JWTs, AWS access keys, GitHub PATs, Slack tokens) with
[REDACTED-*] markers. Fail-open: leaves output untouched on parse error.
Hooks can be filtered with an if clause using permission rule syntax:
{
"matcher": "Bash",
"if": "Bash(git push:*)",
"hooks": [
{ "type": "command", "command": "bash scripts/hooks/notify-push.sh" }
]
}The hook only fires when the matched call satisfies the if rule. Useful
for narrow filters that would otherwise need shell-level matching inside
the hook script.
Hooks can invoke MCP tools directly without spawning a subprocess:
{
"type": "mcp_tool",
"server": "memory",
"tool": "store_observation",
"arguments": { "kind": "session_start" }
}All hook payloads share these fields:
{
"session_id": "...",
"tool_name": "Bash",
"tool_input": { ... },
"tool_output": { ... }
}Stop adds cost info:
{
"num_turns": 12,
"cost": { "total_cost_usd": 0.42, "total_duration_ms": 82000 }
}- Hook stdout is injected into the conversation for
SessionStartandUserPromptSubmit. For other events it's ignored — use stderr for anything the user should see. - Hooks have a 30s timeout by default. Long-running work should detach.
- Hooks run with the user's environment and full file access. Treat them like any other code in your repo.
- If
jqis missing, fail open (exit 0) — don't block legit tool calls over a parse error.