Hooks System
Overview
Hooks are user-defined automated actions that execute at specific lifecycle points in Claude Code. They are deterministic (not advisory) — once configured, they execute strictly and cannot be skipped by Claude.
Four Handler Types
1. Command (Shell)
Runs a local shell command, receives JSON on stdin:
{
"hooks": {
"PreToolUse": [{
"type": "command",
"command": "python3 ~/.claude/hooks/lint-check.py"
}]
}
}
2. HTTP (Network Request)
Sends POST to URL, supports env var interpolation in headers:
{
"hooks": {
"Stop": [{
"type": "http",
"url": "https://hooks.example.com/notify",
"headers": {
"Authorization": "Bearer ${API_TOKEN}"
}
}]
}
}
3. Prompt (LLM Evaluation)
Uses a Claude model to evaluate conditions, returns {ok, reason}:
{
"hooks": {
"PreToolUse": [{
"type": "prompt",
"prompt": "Is this Bash command safe? No delete operations or rm -rf allowed."
}]
}
}
4. Agent (Subagent Verification)
Spawns a subagent with tool access for complex verification:
{
"hooks": {
"PostToolUse": [{
"type": "agent",
"prompt": "Verify the just-edited file passes type checking and linting.",
"tools": ["Bash", "Read"]
}]
}
}
Lifecycle Events
Session Events
| Event | When It Fires | Typical Use |
|---|---|---|
SessionStart |
Session begins or resumes | Initialize env vars, logging |
SessionEnd |
Session terminates | Cleanup, send analytics |
InstructionsLoaded |
CLAUDE.md or rule file loaded | Debug which instructions load |
ConfigChange |
Config file changes during session | Hot-reload configuration |
Tool Events
| Event | When It Fires | Typical Use |
|---|---|---|
PreToolUse |
Before tool call executes (can block) | Security validation, parameter modification |
PostToolUse |
After tool call succeeds | Auto-formatting, notifications |
PostToolUseFailure |
After tool call fails | Error logging, auto-repair |
PermissionRequest |
Permission dialog appears | Auto-allow/deny policies |
Subagent Events
| Event | When It Fires | Typical Use |
|---|---|---|
SubagentStart |
Subagent spawned | Log subagent activity |
SubagentStop |
Subagent finished | Result validation |
TeammateIdle |
Team member about to go idle | Assign new work |
TaskCompleted |
Task marked complete | Quality gates |
Context Events
| Event | When It Fires | Typical Use |
|---|---|---|
PreCompact |
Before context compaction | Save critical info |
PostCompact |
After context compaction | Restore critical info |
UserPromptSubmit |
Prompt submitted, before processing | Input validation |
Stop |
Claude finishes responding | Desktop notifications |
Notification |
Notification sent | Custom notification channels |
Other Events
| Event | When It Fires |
|---|---|
WorktreeCreate / WorktreeRemove |
Worktree created/removed |
Elicitation / ElicitationResult |
MCP server requests user input |
Exit Code Behavior
| Exit Code | Effect |
|---|---|
| 0 | Success, stdout parsed as JSON output |
| 2 | Blocking error — blocks tool calls, denies permissions, prevents stopping |
| Other | Non-blocking error, stderr shown in verbose mode |
PreToolUse JSON Output
PreToolUse is the most powerful hook event — it can return permission decisions and modify parameters:
{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "allow",
"permissionDecisionReason": "Passed safety check",
"updatedInput": { "command": "npm test -- --coverage" },
"additionalContext": "Added coverage flag"
}
}
Permission decision options:
allow: Allow execution (skip user confirmation)deny: Deny executionask: Defer to user decision
Hook Configuration Locations
| Location | Scope |
|---|---|
~/.claude/settings.json |
All projects |
.claude/settings.json |
Single project (shareable) |
.claude/settings.local.json |
Single project (local only) |
| Managed policy settings | Organization-wide |
Plugin hooks/hooks.json |
When plugin enabled |
| Skill/Agent frontmatter | While component active |
Environment Variables
| Variable | Description |
|---|---|
$CLAUDE_PROJECT_DIR |
Project root directory |
${CLAUDE_PLUGIN_ROOT} |
Plugin root directory |
$CLAUDE_ENV_FILE |
Write export VAR=val to persist env vars (in SessionStart) |
Common Input Fields
Every hook receives JSON with these fields:
{
"session_id": "abc123",
"transcript_path": "/path/to/transcript.jsonl",
"cwd": "/current/working/directory",
"permission_mode": "default",
"hook_event_name": "EventName",
"agent_id": "agent-xyz",
"agent_type": "AgentName"
}
Practical Recipes
Auto-format Saved Files
{
"hooks": {
"PostToolUse": [{
"type": "command",
"command": "jq -r '.tool_input.file_path // empty' | xargs -I {} prettier --write {}",
"matcher": { "tool_name": "Edit|Write" }
}]
}
}
Desktop Notification on Completion
{
"hooks": {
"Stop": [{
"type": "command",
"command": "osascript -e 'display notification \"Claude is done\" with title \"Claude Code\"'"
}]
}
}
Block Dangerous Shell Commands
{
"hooks": {
"PreToolUse": [{
"type": "prompt",
"prompt": "Check if this Bash command contains dangerous operations (rm -rf, git push --force, DROP TABLE, etc). Reject if dangerous.",
"matcher": { "tool_name": "Bash" }
}]
}
}
MCP Tool Naming Pattern
MCP tools in hooks use mcp__<server>__<tool> naming — e.g., mcp__memory__create_entities.