obsidian/wiki/agent-sdk/hooks-guide.md
2026-04-17 12:40:31 +01:00

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