290 lines
8.6 KiB
Markdown
290 lines
8.6 KiB
Markdown
---
|
|
title: "Claude Code Hooks — Automate Workflows"
|
|
aliases: [hooks-guide, claude-hooks, lifecycle-hooks]
|
|
tags: [claude-code, hooks, automation, lifecycle, shell, workflow]
|
|
sources: [raw/Automate workflows with hooks.md]
|
|
created: 2026-04-17
|
|
updated: 2026-04-17
|
|
---
|
|
|
|
## Overview
|
|
|
|
Hooks are user-defined shell commands that execute at specific points in Claude Code's lifecycle. They provide **deterministic** control — certain actions always happen, regardless of what the LLM decides to do.
|
|
|
|
Four hook types exist:
|
|
- `"type": "command"` — run a shell command (most common)
|
|
- `"type": "http"` — POST event data to an HTTP endpoint
|
|
- `"type": "prompt"` — single-turn LLM evaluation (Haiku by default)
|
|
- `"type": "agent"` — multi-turn subagent with tool access (60s timeout, 50 tool turns)
|
|
|
|
---
|
|
|
|
## Hook Events Reference
|
|
|
|
| Event | When it fires |
|
|
|-------|--------------|
|
|
| `SessionStart` | Session begins or resumes |
|
|
| `UserPromptSubmit` | Prompt submitted, before Claude processes it |
|
|
| `PreToolUse` | Before a tool call — can block it |
|
|
| `PostToolUse` | After a tool call succeeds |
|
|
| `PostToolUseFailure` | After a tool call fails |
|
|
| `PermissionRequest` | Permission dialog is about to appear |
|
|
| `PermissionDenied` | Tool call denied by auto-mode classifier |
|
|
| `Notification` | Claude is waiting for input |
|
|
| `Stop` | Claude finishes responding |
|
|
| `StopFailure` | Turn ended due to API error |
|
|
| `SubagentStart` / `SubagentStop` | Subagent spawned / finished |
|
|
| `TaskCreated` / `TaskCompleted` | Task lifecycle events |
|
|
| `ConfigChange` | Config file changed during session |
|
|
| `CwdChanged` | Working directory changed |
|
|
| `FileChanged` | Watched file changed on disk |
|
|
| `PreCompact` / `PostCompact` | Before/after context compaction |
|
|
| `InstructionsLoaded` | CLAUDE.md or rules file loaded |
|
|
| `WorktreeCreate` / `WorktreeRemove` | Worktree lifecycle |
|
|
| `Elicitation` / `ElicitationResult` | MCP server requesting user input |
|
|
| `TeammateIdle` | Agent team member going idle |
|
|
| `SessionEnd` | Session terminates |
|
|
|
|
---
|
|
|
|
## Exit Codes & Output
|
|
|
|
| Exit code | Meaning |
|
|
|-----------|---------|
|
|
| `0` | Allow. stdout added to Claude's context (for `SessionStart`, `UserPromptSubmit`) |
|
|
| `2` | Block. Write reason to stderr — Claude receives it as feedback |
|
|
| Other | Allow, but log error. First line of stderr shown in transcript |
|
|
|
|
**Structured JSON output** (exit 0 + JSON to stdout) for fine-grained control:
|
|
|
|
```json
|
|
{
|
|
"hookSpecificOutput": {
|
|
"hookEventName": "PreToolUse",
|
|
"permissionDecision": "deny",
|
|
"permissionDecisionReason": "Use rg instead of grep"
|
|
}
|
|
}
|
|
```
|
|
|
|
`permissionDecision` values for `PreToolUse`: `"allow"` | `"deny"` | `"ask"` | `"defer"` (non-interactive only)
|
|
|
|
> `"allow"` skips the interactive prompt but **does not override** deny rules from settings.
|
|
|
|
---
|
|
|
|
## Matchers
|
|
|
|
Without a matcher, a hook fires on every occurrence of its event. Add `"matcher"` to narrow scope.
|
|
|
|
```json
|
|
{
|
|
"hooks": {
|
|
"PostToolUse": [
|
|
{
|
|
"matcher": "Edit|Write",
|
|
"hooks": [{ "type": "command", "command": "prettier --write ..." }]
|
|
}
|
|
]
|
|
}
|
|
}
|
|
```
|
|
|
|
| Event | Matched field | Example values |
|
|
|-------|--------------|----------------|
|
|
| `PreToolUse`, `PostToolUse`, `PermissionRequest` | tool name | `Bash`, `Edit\|Write`, `mcp__.*` |
|
|
| `SessionStart` | start source | `startup`, `resume`, `clear`, `compact` |
|
|
| `SessionEnd` | end reason | `clear`, `resume`, `logout`, `other` |
|
|
| `ConfigChange` | config source | `user_settings`, `project_settings`, `skills` |
|
|
| `FileChanged` | literal filenames | `.envrc\|.env` |
|
|
| `SubagentStart/Stop` | agent type | `Bash`, `Explore`, `Plan` |
|
|
|
|
**`if` field** (v2.1.85+): filter by tool name + arguments, using permission rule syntax:
|
|
|
|
```json
|
|
{
|
|
"type": "command",
|
|
"if": "Bash(git *)",
|
|
"command": "./.claude/hooks/check-git-policy.sh"
|
|
}
|
|
```
|
|
|
|
Only works on tool events (`PreToolUse`, `PostToolUse`, `PostToolUseFailure`, `PermissionRequest`, `PermissionDenied`).
|
|
|
|
---
|
|
|
|
## Common Recipes
|
|
|
|
### Desktop notification when Claude needs input
|
|
|
|
```json
|
|
{
|
|
"hooks": {
|
|
"Notification": [{
|
|
"matcher": "",
|
|
"hooks": [{
|
|
"type": "command",
|
|
"command": "osascript -e 'display notification \"Claude Code needs your attention\" with title \"Claude Code\"'"
|
|
}]
|
|
}]
|
|
}
|
|
}
|
|
```
|
|
|
|
### Auto-format with Prettier after file edits
|
|
|
|
```json
|
|
{
|
|
"hooks": {
|
|
"PostToolUse": [{
|
|
"matcher": "Edit|Write",
|
|
"hooks": [{
|
|
"type": "command",
|
|
"command": "jq -r '.tool_input.file_path' | xargs npx prettier --write"
|
|
}]
|
|
}]
|
|
}
|
|
}
|
|
```
|
|
|
|
### Re-inject context after compaction
|
|
|
|
```json
|
|
{
|
|
"hooks": {
|
|
"SessionStart": [{
|
|
"matcher": "compact",
|
|
"hooks": [{
|
|
"type": "command",
|
|
"command": "echo 'Reminder: use Bun, not npm. Run bun test before committing.'"
|
|
}]
|
|
}]
|
|
}
|
|
}
|
|
```
|
|
|
|
### Reload direnv when directory changes
|
|
|
|
```json
|
|
{
|
|
"hooks": {
|
|
"CwdChanged": [{
|
|
"hooks": [{
|
|
"type": "command",
|
|
"command": "direnv export bash >> \"$CLAUDE_ENV_FILE\""
|
|
}]
|
|
}]
|
|
}
|
|
}
|
|
```
|
|
|
|
### Auto-approve ExitPlanMode permission
|
|
|
|
```json
|
|
{
|
|
"hooks": {
|
|
"PermissionRequest": [{
|
|
"matcher": "ExitPlanMode",
|
|
"hooks": [{
|
|
"type": "command",
|
|
"command": "echo '{\"hookSpecificOutput\": {\"hookEventName\": \"PermissionRequest\", \"decision\": {\"behavior\": \"allow\"}}}'"
|
|
}]
|
|
}]
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Hook Scopes / Configuration Files
|
|
|
|
| File | Scope | Shareable |
|
|
|------|-------|-----------|
|
|
| `~/.claude/settings.json` | All projects | No (local machine) |
|
|
| `.claude/settings.json` | Single project | Yes (commit to repo) |
|
|
| `.claude/settings.local.json` | Single project | No (gitignored) |
|
|
| Managed policy settings | Organization-wide | Yes (admin-controlled) |
|
|
| Plugin `hooks/hooks.json` | When plugin enabled | Yes |
|
|
| Skill/agent frontmatter | While active | Yes |
|
|
|
|
Disable all hooks: set `"disableAllHooks": true` in any settings file.
|
|
|
|
---
|
|
|
|
## Prompt-Based & Agent-Based Hooks
|
|
|
|
**Prompt hook** — LLM evaluates a condition and returns `{"ok": true}` or `{"ok": false, "reason": "..."}`:
|
|
|
|
```json
|
|
{
|
|
"hooks": {
|
|
"Stop": [{
|
|
"hooks": [{
|
|
"type": "prompt",
|
|
"prompt": "Check if all tasks are complete. If not, respond with {\"ok\": false, \"reason\": \"what remains\"}."
|
|
}]
|
|
}]
|
|
}
|
|
}
|
|
```
|
|
|
|
**Agent hook** — spawns a subagent that can read files and run tools before deciding:
|
|
|
|
```json
|
|
{
|
|
"hooks": {
|
|
"Stop": [{
|
|
"hooks": [{
|
|
"type": "agent",
|
|
"prompt": "Verify that all unit tests pass. Run the test suite. $ARGUMENTS",
|
|
"timeout": 120
|
|
}]
|
|
}]
|
|
}
|
|
}
|
|
```
|
|
|
|
Use prompt hooks when the event data alone is enough. Use agent hooks when you need to inspect actual codebase state.
|
|
|
|
---
|
|
|
|
## Troubleshooting
|
|
|
|
| Problem | Fix |
|
|
|---------|-----|
|
|
| Hook not firing | Check `/hooks` menu, verify matcher case-sensitivity, confirm correct event type |
|
|
| "hook error" in transcript | Test manually: `echo '{"tool_name":"Bash",...}' \| ./hook.sh; echo $?` |
|
|
| `/hooks` shows nothing | Validate JSON (no trailing commas/comments), check file location |
|
|
| Stop hook infinite loop | Parse `stop_hook_active` from stdin, exit 0 if `true` |
|
|
| JSON validation failed | Shell profile `echo` statements pollute stdout — wrap in `if [[ $- == *i* ]]` |
|
|
| Debug | Run `claude --debug-file /tmp/claude.log`, then `tail -f /tmp/claude.log` |
|
|
|
|
**Permission interaction:** `PreToolUse` hooks fire before permission-mode checks. A hook returning `deny` blocks even in `bypassPermissions` mode. A hook returning `allow` cannot override deny rules from settings — hooks tighten but cannot loosen.
|
|
|
|
---
|
|
|
|
## Key Takeaways
|
|
|
|
- Hooks run **deterministically** at lifecycle events — LLM cannot skip them
|
|
- Use `PreToolUse` + exit 2 to block commands; use `PostToolUse` to react after
|
|
- `matcher` filters by tool name (regex); `if` field filters by tool name + arguments together
|
|
- Multiple hooks for the same event run **in parallel**; decisions resolve to most restrictive
|
|
- `PostToolUse` cannot undo already-executed actions
|
|
- `PermissionRequest` hooks don't fire in non-interactive (`-p`) mode — use `PreToolUse` instead
|
|
- Hook stdout goes to Claude's context; stderr goes to Claude as error feedback (on exit 2)
|
|
- Three LLM-powered types: `prompt` (single call), `agent` (multi-turn with tools), `http` (external service)
|
|
|
|
---
|
|
|
|
## Related
|
|
|
|
- [[wiki/agent-sdk/overview|Agent SDK Overview]]
|
|
- [[wiki/agent-sdk/agent-skills-plugins|Agent Skills & Plugins]]
|
|
- [[wiki/agent-sdk/skills-in-sdk|Skills in the SDK]]
|
|
- [[wiki/tech-patterns/_index|Tech Patterns]]
|
|
|
|
---
|
|
|
|
## Sources
|
|
|
|
- `raw/Automate workflows with hooks.md` — clipped from https://code.claude.com/docs/en/hooks-guide
|