Hooks
Hooks are shell commands Claude Code runs at specific events — before a tool call, after a file edit, when the session ends. They're how you turn Claude from a smart assistant into an integrated piece of your workflow: format on save, notify when long-running tasks finish, block dangerous operations, audit everything.
On this page
1. Why hooks (vs skills, vs commands)
Skills and commands change what Claude does. Hooks fire when Claude does something — automatic, no LLM in the loop, deterministic.
| Need | Reach for |
|---|---|
| "Format every file Claude edits." | Hook (PostToolUse on Edit/Write) |
| "Notify me when long-running prompts finish." | Hook (Stop) |
| "Run typecheck after edits and feed the result back." | Hook (PostToolUse + return JSON) |
| "Refuse to delete prod env vars." | Hook (PreToolUse, return exit 1) |
| "Capture an audit log of every file edit for compliance." | Hook (PostToolUse) |
2. The event lifecycle
| Event | Fires when | Common use |
|---|---|---|
SessionStart | Claude Code starts in a directory. | Print today's tasks, warm caches, pull latest git. |
UserPromptSubmit | You hit Enter on a message. | Inject context, redact secrets, log prompts. |
PreToolUse | Claude is about to call a tool. | Block / validate / require confirmation. |
PostToolUse | Claude just finished a tool call. | Format the file, run typecheck, audit-log. |
Notification | Claude needs your attention (permission, idle). | Beep, push notification, Slack message. |
Stop | Claude finished a turn. | Notify you, compute cost, take a screenshot. |
SubagentStop | A subagent finished. | Same as Stop but per-subagent. |
SessionEnd | Session closes. | Summarize, log, sync state. |
3. Configuration
Hooks live in ~/.claude/settings.json (user) or .claude/settings.json (project) under the hooks key.
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{ "type": "command", "command": "prettier --write \"$CLAUDE_FILE\"" }
]
}
],
"Stop": [
{
"hooks": [
{ "type": "command", "command": "osascript -e 'display notification \"Claude is done\" with title \"Claude Code\"'" }
]
}
]
}
}
Each event maps to a list of matcher groups. Each group has one matcher (regex) and one or more commands. Commands run in your shell with a working directory of the project root.
4. Matchers
The matcher is a regex against the tool name (for PreToolUse / PostToolUse). Common patterns:
| Matcher | Fires for |
|---|---|
".*" or omitted | Every tool. |
"Bash" | Just Bash calls. |
"Edit|Write|MultiEdit" | Any file-mutating tool. |
"mcp__github__.*" | Any tool from the GitHub MCP server. |
"Read|Glob|Grep" | Read-only ops. |
Other events (Stop, Notification, SessionStart, etc.) don't take matchers — just hook arrays.
5. Environment variables your hook sees
| Variable | What it is |
|---|---|
CLAUDE_PROJECT_DIR | The project root. |
CLAUDE_TOOL_NAME | Tool that fired the event (PreToolUse/PostToolUse). |
CLAUDE_FILE | For file ops, the path that was edited / written. |
CLAUDE_COMMAND | For Bash hooks, the command that ran. |
CLAUDE_SESSION_ID | Stable id for the current session. |
| stdin | Hook also gets the full event JSON on stdin — useful for richer logic. |
6. Practical examples
Format every Claude-edited file
{
"PostToolUse": [{
"matcher": "Edit|Write|MultiEdit",
"hooks": [
{ "type": "command", "command": "case \"$CLAUDE_FILE\" in *.ts|*.tsx|*.js|*.jsx|*.json|*.md) prettier --write \"$CLAUDE_FILE\" ;; *.py) ruff format \"$CLAUDE_FILE\" ;; *.go) gofmt -w \"$CLAUDE_FILE\" ;; esac" }
]
}]
}
macOS notification when Claude finishes
{
"Stop": [{
"hooks": [{ "type": "command", "command": "osascript -e 'display notification \"Done\" with title \"Claude Code\" sound name \"Glass\"'" }]
}]
}
Block force-pushes to main
{
"PreToolUse": [{
"matcher": "Bash",
"hooks": [{
"type": "command",
"command": "echo \"$CLAUDE_COMMAND\" | grep -qE 'git push --force.*(main|master)' && { echo 'Refusing: force-push to main blocked' >&2; exit 2; }; true"
}]
}]
}
Exit code 2 = block. Exit code 0 = allow. The stderr text is shown to Claude as the reason.
Audit log of every file edit
{
"PostToolUse": [{
"matcher": "Edit|Write|MultiEdit",
"hooks": [{
"type": "command",
"command": "echo \"$(date -Iseconds) $CLAUDE_SESSION_ID $CLAUDE_TOOL_NAME $CLAUDE_FILE\" >> ~/.claude/audit.log"
}]
}]
}
Inject "today's date" into every prompt
{
"UserPromptSubmit": [{
"hooks": [{
"type": "command",
"command": "echo '{\"appendToPrompt\":\"(today: '\"$(date +%F)\"')\"}'"
}]
}]
}
For events that support it, returning JSON on stdout modifies behavior — here, appending context to the prompt before Claude sees it.
Typecheck after edits and report back
{
"PostToolUse": [{
"matcher": "Edit|Write|MultiEdit",
"hooks": [{
"type": "command",
"command": "if [[ \"$CLAUDE_FILE\" == *.ts || \"$CLAUDE_FILE\" == *.tsx ]]; then npx tsc --noEmit 2>&1 | head -50; fi"
}]
}]
}
Output goes back to Claude, who'll see typecheck errors and self-correct.
7. Blocking vs non-blocking
| Exit code | Effect |
|---|---|
0 | Allow, attach stdout as info. |
2 | Block the tool call (PreToolUse). Stderr is shown to Claude as the reason. |
| Other non-zero | Treated as a soft error — logged, but doesn't block. |
For events that don't gate an action (Stop, SessionEnd, Notification), exit codes are advisory.
8. Security model
$PATH.- Project hooks need consent. When Claude Code loads a project that has hooks in
.claude/settings.json, it surfaces them and asks before enabling. Don't blindly accept. - Pin commands to absolute paths for sensitive hooks.
/usr/local/bin/prettierbeatsprettierwhen an attacker could shadow your PATH. - Avoid eval'ing untrusted input. Don't pipe
$CLAUDE_COMMANDdirectly intoevalorbash -c. - Use
denyin permissions alongside PreToolUse hooks for defense in depth.
9. Debugging hooks
- Run Claude with
--debug— hook invocations + exit codes are logged. - Have your hook write to a temp file first:
echo "$CLAUDE_FILE" >> /tmp/claude-hook.log. - Test the command in your shell with the env vars set:
CLAUDE_FILE=src/foo.ts prettier --write "$CLAUDE_FILE". - If a hook never fires, check the event name (case-sensitive) and the matcher regex against the actual tool name (run
/helpto see exact names).