Hooks System
Lifecycle events
Hooks are shell commands that automatically execute on certain events in Claude Code.
Use cases
- • Auto-format after Edit
- • Bash command validation
- • Slack notifications
- • Action logging
Limitations
- • Execute synchronously
- • 600 second default timeout
- • Can block operations
How hooks work and why you need them
Hooks in Claude Code are plain shell commands that the agent runs automatically in response to specific events in a session's lifecycle. Instead of asking the assistant to "remember to format the file" or "check the command before running it," you describe a rule once and it fires deterministically on every matching event. The key idea is that hooks run outside the model: they are code you control, so their behaviour is predictable and never depends on whether the language model happens to recall a step. That makes hooks an ideal way to bake guaranteed validation, formatting, and alerting into your workflow.
There are many events — over thirty. Common ones include the session events SessionStart and SessionEnd, which fire when work begins and ends and suit environment setup or sending metrics. Tool events — PreToolUse and PostToolUse — wrap any tool call, such as Edit or Bash. Beyond those there are UserPromptSubmit (when you send a message), Notification (when Claude wants to alert you), Stop and SubagentStop, PermissionRequest, PreCompact, and more. The matcher field narrows what triggers a tool hook: "*" (or an empty string, or omitting it) means all tools, "Bash" or "Edit|Write" is an exact name or pipe-separated list, and anything with other characters is treated as a regular expression (e.g. "mcp__.*").
Walk through the config examples on this page. The settings.json block describes a PostToolUse event: after every edit to a file ending in .ts, .tsx, .js, or .jsx, it runs prettier --write. The hook receives the event data (including the file path) as JSON on stdin — there is no {{path}} template placeholder; for project and plugin paths you have the variables ${CLAUDE_PROJECT_DIR} and ${CLAUDE_PLUGIN_ROOT}. The second example is a PreToolUse hook for the Bash tool: it intercepts the command before execution, scans it for dangerous patterns like rm -rf, and on a match exits with code 2 — that is what blocks the operation, and the text on stderr is fed back to Claude. Exit code 0 means success (the operation proceeds), while any other non-zero code (such as 1) is a non-blocking error: the operation still runs. The main pitfalls: hooks run synchronously and a command hook's default timeout is 600 seconds, so heavy commands can stall the session for a long time; an overly broad matcher can fire more often than intended; and you should log errors inside a hook, or a silent block becomes hard to debug. Always run a hook config locally before committing it.
SessionStartWhen Claude Code session starts
SessionEndWhen session ends
PreToolUseBefore tool is called
PostToolUseAfter tool is called
UserPromptSubmitWhen user sends a message
NotificationOn notifications from Claude
StopWhen Claude stops
Automatic file formatting after editing:
// ~/.claude/settings.json (or .claude/settings.json)
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "jq -r '.tool_input.file_path' | { read f; case \"$f\" in *.ts|*.tsx|*.js|*.jsx) npx prettier --write \"$f\";; esac; }"
}
]
}
]
}
}PreToolUse blocks an operation by exiting with code 2:
// settings.json — block dangerous commands
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "cmd=$(jq -r '.tool_input.command'); if echo \"$cmd\" | grep -qE 'rm -rf|drop database'; then echo 'BLOCKED: dangerous command' >&2; exit 2; fi"
}
]
}
]
}
}Exit code 0
Success — operation continues
Exit code 2
Operation is blocked, stderr is sent to Claude
Hooks are configured in settings.json under the hooks key:
~/.claude/settings.json # user (global) settings .claude/settings.json # project settings (committed) .claude/settings.local.json # project settings (not committed) # all of them hold hooks under a single "hooks" key
matcherWhich tools to match ("*", "Bash", "Edit|Write", regex)typeHook type — usually "command"commandShell command to executeHook Best Practices
- 1.Keep hooks fast — they block the main thread
- 2.Use matcher for precise triggering
- 3.Log hook errors for debugging
- 4.Test hooks locally before committing
Frequently asked questions
What are hooks in Claude Code?
Hooks are plain shell commands that Claude Code runs automatically on specific session events: session start and end, before and after a tool call, when you submit a message, or when the agent stops. They run outside the model, so they fire deterministically and never depend on whether the assistant recalls a step.
How do I set up a hook to auto-format files after editing?
Create .claude/hooks/format-on-edit.json with a PostToolUse event, set match to the Edit tool and a glob path pattern (e.g. **/*.{ts,tsx,js,jsx}), and set command to a formatter such as npx prettier --write {{path}}. The {{path}} placeholder is replaced with the path of the edited file.
How can a PreToolUse hook block dangerous commands?
Add a PreToolUse hook for the Bash tool that intercepts the command before execution. If it finds a dangerous pattern like rm -rf, the hook returns exit code 1 and the operation is blocked. An exit code of 0 lets the operation proceed, giving you a guardrail against accidental data loss.
What are the limitations of Claude Code hooks?
Hooks run synchronously with a roughly 60-second timeout, so heavy commands stall the whole session. An overly broad match can trigger more often than intended, and you should log errors inside a hook or a silent block becomes hard to debug. Always test a hook config locally before committing it.
This lesson is part of a structured LLM course.
My Learning Path