| name | plugin-hooks |
| description | Authoritative guide to Claude Code hooks โ event-driven scripts that execute before or after tool calls, session events, file changes, and more. Use when writing a PreToolUse hook to block dangerous commands, a PostToolUse hook to auto-format after edits, a SessionStart hook to inject context, a Stop hook for session loops, a Notification hook for desktop alerts, a FileChanged hook for reactive environments, a WorktreeCreate hook for custom worktree provisioning, or current documented hook events. Covers handler types (command, http, mcp_tool, prompt, agent), matcher syntax (exact/OR-list/regex), exit code semantics, and JSON output schema. NOT for designing hook script content for a specific domain (use the domain skill) โ this skill covers hook mechanics and authoring only. |
Plugin Hooks
The most powerful and most trap-laden extension type. Exit code 1 does not block โ only exit code 2 blocks. Matcher syntax has three modes. updatedInput replaces the entire tool input object, not just one field. These three facts cause most hook bugs. Learn them first.
Authoritative source: https://code.claude.com/docs/en/hooks
When to use this skill
- Writing any hook configuration (hooks/hooks.json, settings.json)
- Debugging "my hook isn't blocking" or "my hook isn't firing"
- Choosing between hook event types for a specific capability
- Understanding exit code behavior (exit 1 vs exit 2 distinction)
- Designing safe auto-approve flows for PermissionRequest
- Setting up file-watching or cwd-change hooks
When NOT to use this skill
- What hook script logic to write (domain-specific) โ use the domain skill
- MCP server authoring โ use
plugin-architecture or mcp-server
- Full plugin structure โ use
plugin-composition
Core principles
- Exit code 1 does not block. Only exit code 2 blocks a tool call, prompt, or stop. Exit 1 is a non-blocking error โ the action proceeds. This is the #1 source of hook bugs.
- Matcher syntax has three modes.
* or omitted = match all. Letters/digits/underscore/pipe only = exact string or |-separated list. Any other character = JavaScript regex. mcp__memory is an exact match (matches no real tool); use mcp__memory__.* for regex.
updatedInput replaces the entire tool input. In PreToolUse, returning updatedInput in hookSpecificOutput replaces ALL fields. You must echo back unchanged fields or they are lost.
- Async hooks cannot block. If
async: true, the hook runs after the action has already proceeded. Never use async for security/guardrail hooks.
Core events (most common in practice)
The full documented event set changes over time. references/hook-event-reference.md tracks the current catalog; this section covers the events practitioners reach for most often.
PreToolUse
Fires before any tool call. Can block, modify input, or allow. The most powerful event.
{
"matcher": "Bash",
"hooks": [{"type": "command", "command": "${CLAUDE_PLUGIN_ROOT}/scripts/check.sh"}]
}
Input includes: tool_name, tool_input (full schema), tool_use_id.
Decision output:
{"hookSpecificOutput": {"hookEventName": "PreToolUse", "permissionDecision": "deny", "permissionDecisionReason": "Blocked"}}
Precedence when multiple hooks run: deny > defer > ask > allow.
PostToolUse
Fires after a tool succeeds. Cannot block. Common use: auto-format, logging, notifications.
{"matcher": "Edit|Write", "hooks": [{"type": "command", "async": true, "command": "prettier --write \"$file_path\""}]}
PermissionRequest
Fires when a permission dialog would appear. Can auto-approve or deny without showing the dialog.
{"hookSpecificOutput": {"hookEventName": "PermissionRequest", "decision": {"behavior": "allow"}}}
UserPromptSubmit
Fires before Claude processes a user prompt. Can block or add context. No matcher support โ fires on every prompt.
SessionStart
Fires at session start, resume, clear, or compact. Can inject context Claude will see.
{"matcher": "compact", "hooks": [{"type": "command", "command": "${CLAUDE_PLUGIN_ROOT}/scripts/inject-context.sh"}]}
Exit 0 stdout is added to Claude's context for SessionStart.
Stop
Fires when Claude finishes responding. Can prevent Claude from stopping to continue working.
โ ๏ธ Infinite loop trap: if your Stop hook always signals "keep going", Claude loops forever. Check stop_hook_active in input:
input=$(cat)
if echo "$input" | python3 -c "import json,sys; d=json.load(sys.stdin); sys.exit(0 if d.get('stop_hook_active') else 1)" 2>/dev/null; then
exit 0
fi
Notification
Fires for: permission_prompt, idle_prompt, auth_success, elicitation_dialog. Common use: desktop notifications.
FileChanged
Fires when a watched file changes on disk. The matcher both builds the watch list (literal filenames split by |) and filters which handlers run. Example watching .envrc:
{"matcher": ".envrc", "hooks": [{"type": "command", "command": "direnv export bash >> \"$CLAUDE_ENV_FILE\""}]}
SubagentStop
Fires when a subagent finishes. Useful for aggregating subagent results.
WorktreeCreate
Fires when a worktree is created. Any non-zero exit fails worktree creation. The hook must print the absolute path of the created worktree to stdout:
echo "/absolute/path/to/worktree"
Hook handler types (quick reference)
| Type | Use when | Key fields |
|---|
command | Shell script, full OS access | command, async, shell, timeout |
http | Remote service, shared state | url, headers, allowedEnvVars, timeout |
mcp_tool | Delegate the hook decision to a configured MCP tool | server, tool, arguments, timeout |
prompt | LLM evaluation | prompt (use $ARGUMENTS), model, timeout (30s default) |
agent | Multi-step tool-using verification | prompt (use $ARGUMENTS), model, timeout (60s default) |
See references/hook-handler-types.md for full schemas and examples.
Matcher syntax (quick reference)
| Value | Interpreted as |
|---|
*, "", or omitted | Match all โ fires on every occurrence |
Letters/digits/_/| only | Exact string or |-separated list: Bash, Edit|Write |
| Any other character | JavaScript regex: ^Notebook, mcp__memory__.* |
Common mistake: mcp__memory (underscores only) is evaluated as an exact string. It never matches any real tool because real tools are mcp__memory__create_entities etc. Use mcp__memory__.* (has .*) for regex.
Exit code contract
Exit 0 โ success; parse stdout for JSON output
Exit 2 โ blocking error; ignore stdout; use stderr as the error message
Exit 1 โ NON-BLOCKING error; the action proceeds anyway; stderr shown in transcript
Timeout โ treated as non-blocking error
Not every event can be blocked. See references/hook-event-reference.md for the per-event blocking table.
Plugin hooks configuration
Hooks ship with a plugin in hooks/hooks.json at the plugin root. Example:
{
"description": "Format TypeScript files after edits",
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [{"type": "command", "command": "${CLAUDE_PLUGIN_ROOT}/scripts/format.sh"}]
}
]
}
}
Use ${CLAUDE_PLUGIN_ROOT} for paths to scripts bundled with the plugin. Use ${CLAUDE_PLUGIN_DATA} for state that should survive plugin updates.
References
| File | Contents |
|---|
references/hook-event-reference.md | Current hook event catalog with schemas, blocking behavior, matcher semantics |
references/hook-handler-types.md | Command, http, mcp_tool, prompt, agent โ full schemas, examples, security |
references/hook-anti-patterns.md | Exit code 1 trap, infinite Stop loops, shell profile pollution, partial updatedInput, and 10+ more |
references/hook-testing-patterns.md | /hooks menu, debug log, test_hook.sh usage, stdin replay |
Plugin-Dev Authoring Toolkit by Viktor Bezdek โ licensed under MIT.