8.6 KiB
| title | aliases | tags | sources | created | updated | ||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Claude Code Hooks — Automate Workflows |
|
|
|
2026-04-17 | 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:
{
"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.
{
"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:
{
"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
{
"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
{
"hooks": {
"PostToolUse": [{
"matcher": "Edit|Write",
"hooks": [{
"type": "command",
"command": "jq -r '.tool_input.file_path' | xargs npx prettier --write"
}]
}]
}
}
Re-inject context after compaction
{
"hooks": {
"SessionStart": [{
"matcher": "compact",
"hooks": [{
"type": "command",
"command": "echo 'Reminder: use Bun, not npm. Run bun test before committing.'"
}]
}]
}
}
Reload direnv when directory changes
{
"hooks": {
"CwdChanged": [{
"hooks": [{
"type": "command",
"command": "direnv export bash >> \"$CLAUDE_ENV_FILE\""
}]
}]
}
}
Auto-approve ExitPlanMode permission
{
"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": "..."}:
{
"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:
{
"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; usePostToolUseto react after matcherfilters by tool name (regex);iffield filters by tool name + arguments together- Multiple hooks for the same event run in parallel; decisions resolve to most restrictive
PostToolUsecannot undo already-executed actionsPermissionRequesthooks don't fire in non-interactive (-p) mode — usePreToolUseinstead- 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
- wiki/agent-sdk/agent-skills-plugins
- wiki/agent-sdk/skills-in-sdk
- wiki/tech-patterns/_index
Sources
raw/Automate workflows with hooks.md— clipped from https://code.claude.com/docs/en/hooks-guide