name: claude-code-hooks description: "Use when configuring automated lifecycle commands (hooks) in Claude Code settings.json — event types, matcher syntax, exit codes, JSON output, path placeholders." user-invocable: true
Claude Code Hooks
Hooks are shell commands, HTTP endpoints, or MCP tools that execute automatically at lifecycle points: before/after tool calls, on session start/end, when config changes, and on other events. The harness controls execution; Claude cannot suppress them.
Event Types
Hooks fire at specific cadences:
Once per session:
SessionStart— beginning, resume, or after compactionSessionEnd— on session close
Once per turn:
UserPromptSubmit— before Claude reads prompt (can block)UserPromptExpansion— before user command expands to prompt (can block)Stop— after Claude respondsStopFailure— if response failed
Per tool call:
PreToolUse— before tool executes (can block)PermissionRequest— when permission dialog appearsPermissionDenied— when tool denied; can return{retry: true}PostToolUse— after tool succeedsPostToolUseFailure— if tool failedPostToolBatch— after parallel batch resolves
Async/file watching:
FileChanged— watched file modified on disk (matcher specifies files)CwdChanged— working directory changedConfigChange— settings/skills file modifiedInstructionsLoaded— CLAUDE.md or rules/*.md loadedNotification— when Claude Code sends notification (matcher: notification type)MessageDisplay— while assistant message displaysSubagentStart— subagent spawned (matcher: agent type)SubagentStop— subagent finishedTaskCreated,TaskCompleted— task lifecycleSetup— on--init-onlyor CI setup modePreCompact,PostCompact— context compaction boundaryElicitation,ElicitationResult— MCP server user inputWorktreeCreate,WorktreeRemove— git worktree lifecycleTeammateIdle— agent team teammate idle
Configuration Locations
~/.claude/settings.json— user-wide (all projects).claude/settings.json— project-wide (shareable, commitable).claude/settings.local.json— project-specific (gitignored)- Plugin
hooks/hooks.json— loaded at plugin install - Skill/agent YAML frontmatter — active while component loaded
Hook Structure
{
"hooks": {
"EventName": [
{
"matcher": "ToolName|OtherTool",
"hooks": [
{
"type": "command|http|mcp_tool|prompt|agent",
"command": "script.sh",
"timeout": 30,
"statusMessage": "Running validation...",
"async": false,
"if": "Bash(git *)"
}
]
}
]
}
}
Matcher Syntax
- Empty or
"*"— matches all - Alphanumeric/
_/|— exact match or literal alternatives:Bash,Edit|Write,mcp__github__create_issue - Other chars (including regex syntax) — regex:
^Notebook,.envrc|.env,mcp__.*__write.* - SessionStart matchers — special:
startup|resume|clear|compact - Notification matchers —
permission_prompt,idle_prompt,auth_success,elicitation_dialog,elicitation_complete,elicitation_response
Command Hook Type
Exec form (preferred; safe for paths with special chars):
{
"type": "command",
"command": "node",
"args": ["${CLAUDE_PLUGIN_ROOT}/script.js", "--fix"]
}
Spawns directly without shell; ${} placeholders safe.
Shell form (full features like pipes, globs):
{
"type": "command",
"command": "grep 'error' log.txt | wc -l"
}
Runs in shell; supports &&, pipes, * globs. Avoid untrusted paths.
Common fields:
timeout— seconds before kill (default: varies by event)statusMessage— user-visible spinner textasync— run in background without blockingasyncRewake— background task, wake Claude on exit 2if— permission rule:Bash(rm **),Edit(*.ts),Bash(git *)once— run once per session, then remove (skills only)shell—bash(default) orpowershell
Path Placeholders
${CLAUDE_PROJECT_DIR}— project root${CLAUDE_PLUGIN_ROOT}— plugin installation directory${CLAUDE_PLUGIN_DATA}— plugin persistent data dir
Always use exec form with args when using placeholders.
Windows Polyglot Pattern
Claude Code on Windows auto-prepends bash to .sh commands. Override by using extensionless filenames with a wrapper:
{
"command": "\"${CLAUDE_PLUGIN_ROOT}/hooks/run-hook.cmd\" session-start"
}
Wrapper (run-hook.cmd): batch block for cmd.exe, shell code for bash:
: << 'CMDBLOCK'
@echo off
if exist "C:\Program Files\Git\bin\bash.exe" (
"C:\Program Files\Git\bin\bash.exe" "%~dp0%~1" %2 %3
exit /b %ERRORLEVEL%
)
where bash >nul 2>nul && bash "%~dp0%~1" %2 %3 && exit /b %ERRORLEVEL%
exit /b 0
CMDBLOCK
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
exec bash "${SCRIPT_DIR}/$1" "${@:2}"
The : << 'CMDBLOCK' trick: cmd.exe runs batch; bash sees : (no-op) and runs Unix shell code.
Exit Code Protocol
| Code | Meaning |
|---|---|
| 0 | Success. Harness parses stdout for JSON output. |
| 2 | Blocking error. Stderr shown to user. Hook can deny action or block turn. |
| Other | Non-blocking error. First line of stderr shown, execution continues. |
Common Input (JSON stdin)
All hooks receive:
session_id— unique identifiercwd— current working directorypermission_mode—default,plan,acceptEdits,auto,dontAsk,bypassPermissionshook_event_name— event nameeffort.level—low,medium,high,xhigh,maxtranscript_path— path to session transcript (JSONL)agent_id,agent_type— when inside subagent- Event-specific fields (e.g.,
tool_name,tool_inputforPreToolUse)
JSON Output (Exit 0 Only)
Exit 0 with valid JSON stdout to control harness. Output format depends on your platform:
Claude Code (CLAUDE_PLUGIN_ROOT set):
{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"additionalContext": "Context for Claude",
"permissionDecision": "deny",
"permissionDecisionReason": "Blocked by policy"
}
}
Cursor (CURSOR_PLUGIN_ROOT set):
{
"additional_context": "Context for Claude"
}
Copilot CLI (SDK standard):
{
"additionalContext": "Context for Claude"
}
Universal fields:
continue: false— stop entire turnstopReason— message when stoppingsuppressOutput: true— hide from transcriptsystemMessage— warning shown to userterminalSequence— OSC codes (desktop notification, window title)
HTTP Hook Type
{
"type": "http",
"url": "http://localhost:8080/hook",
"headers": {"Authorization": "Bearer ${TOKEN}"},
"allowedEnvVars": ["TOKEN"]
}
POST with stdin JSON. Whitelist env vars to pass. Return 2xx with JSON body for decisions.
MCP Tool Hook Type
{
"type": "mcp_tool",
"server": "my_mcp_server",
"tool": "tool_name",
"input": {"file_path": "${tool_input.file_path}"}
}
Call tool on connected MCP server. Fields can interpolate from stdin.
Prompt/Agent Hook Types
{
"type": "prompt",
"prompt": "Should this be allowed? $ARGUMENTS",
"model": "fast-model"
}
Sends yes/no question to Claude. Rarely needed; prefer command hooks for determinism.
Real Example: Block rm -rf
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"if": "Bash(rm -rf *)",
"hooks": [{
"type": "command",
"command": "node",
"args": ["${CLAUDE_PROJECT_DIR}/.claude/hooks/block-rm.js"]
}]
}
]
}
}
Exit code 2 to deny; stderr message shown.
Disable All Hooks
{
"disableAllHooks": true
}
Top-level key to skip all hooks (useful for debugging).
When to Use Hooks
- Deterministic automation ("each time X, always do Y")
- Security gates (block dangerous commands, validate secrets before commits)
- Context injection (SessionStart loads docs, specs, templates)
- Auto-validation (PostToolUse runs tests, blocks if fails)
- Observability (log commands, audit config changes)
Don't use hooks for:
- One-off tasks Claude should handle directly
- Business logic that belongs in code
- Anything requiring complex reasoning (use prompt/agent types sparingly)
Debug Hooks
/hookscommand: browse configured hooks, view matchers/handlers- Exit 2 blocks silently; non-0 shows error in transcript
- Test JSON with
jqbefore deploying - Check script permissions on Unix:
chmod +x script.sh - Use
timeoutto kill runaway scripts - Hook stderr shown in transcript (unless
suppressOutput: true)
Security
Hooks execute with your user permissions. Always:
- Store hook scripts in
.claude/or plugin dirs (not world-writable) - Never call untrusted executables
- Whitelist env vars in HTTP hooks
- Review hook code before enabling