Hooks are user-defined shell commands, HTTP endpoints, LLM prompts, or agents that execute automatically at specific points in the Claude Code lifecycle. They enable automation, validation, security controls, and custom workflows.
Hook Types
1. Command Hooks (type: "command")
Execute shell commands. Receive JSON input via stdin, return results via exit codes and stdout.
{
"type": "command",
"command": ".claude/hooks/validator.sh",
"timeout": 30,
"async": false,
"once": false,
"statusMessage": "Running validation..."
}
Parameters:
command(string): Script path. Supports$CLAUDE_PROJECT_DIR,${CLAUDE_PLUGIN_ROOT}.timeout(number): Max runtime in seconds (default: 600s).async(boolean): Async background execution (default: false).once(boolean): Run once per session (default: false).statusMessage(string): Custom status message during execution.
2. HTTP Hooks (type: "http")
Send POST requests to URLs with JSON payload.
{
"type": "http",
"url": "http://localhost:8080/hooks/validate",
"headers": {
"Authorization": "Bearer $MY_TOKEN"
},
"allowedEnvVars": ["MY_TOKEN"],
"timeout": 30
}
Parameters:
url(string): HTTP endpoint. Supports environment variable interpolation.headers(object): HTTP headers. Variables are interpolated.allowedEnvVars(array): Whitelist of env vars for header interpolation.timeout(number): Max runtime in seconds (default: 30s).
3. Prompt Hooks (type: "prompt")
Single-turn evaluation by Claude with yes/no decision.
{
"type": "prompt",
"prompt": "Should this command be allowed? $ARGUMENTS",
"model": "claude-opus",
"timeout": 30
}
Parameters:
prompt(string): The prompt.$ARGUMENTSreplaced with hook input.model(string):claude-opus,claude-sonnet,claude-haiku.timeout(number): Max runtime in seconds (default: 30s).
4. Agent Hooks (type: "agent")
Spawn subagents for complex verification with tool access.
{
"type": "agent",
"prompt": "Verify that tests pass",
"context": "fork",
"agent": "Plan"
}
Parameters:
prompt(string): Instructions for subagent.context(string):forkfor isolated context.agent(string):Explore,Plan,general-purpose.
Hook Events — Complete List
Session Events
| Event | Matcher | Blockable | Description |
|---|---|---|---|
SessionStart |
startup|resume|clear|compact | No | New/resumed session. Env var: CLAUDE_ENV_FILE |
SessionEnd |
clear|logout|other | Yes (Exit 2) | Session termination. Hard timeout: 1.5s. |
InstructionsLoaded |
— | No | CLAUDE.md/Rules loaded. |
Tool-Execution Events
| Event | Matcher | Blockable | Description |
|---|---|---|---|
PreToolUse |
Tool name (Bash, Edit|Write, mcp__.*) | Yes (Exit 2) | Before tool execution. Env: CLAUDE_TOOL_NAME, CLAUDE_TOOL_INPUT. |
PostToolUse |
Tool name | No | After successful execution. |
PostToolUseFailure |
Tool name | No | After failed execution. |
PermissionRequest |
Tool name | Yes (Exit 2) | Permission dialog shown. |
Agent Events
| Event | Matcher | Blockable | Description |
|---|---|---|---|
SubagentStart |
Agent type (Explore, Plan, ...) | No | Subagent spawned. |
SubagentStop |
Agent type | Yes (Exit 2) | Subagent finished. |
Stop |
— | Yes (Exit 2) | Main agent stopped. |
StopFailure |
Error type (rate_limit, auth_failed, server_error, other) | No | Turn ended due to API error. |
Configuration & Context
| Event | Matcher | Blockable | Description |
|---|---|---|---|
ConfigChange |
user_settings|project_settings|policy_settings | Yes (Exit 2) | Config file changed. |
Notification |
permission_prompt|idle_prompt|other | No | Notification displayed. |
PreCompact |
— | No | Context compaction starting. |
PostCompact |
— | No | Context compaction finished. |
Quality Gates
| Event | Matcher | Blockable | Description |
|---|---|---|---|
TeammateIdle |
— | No | Team member becoming idle. |
TaskCompleted |
— | No | Task marked complete. |
Git Worktree Events
| Event | Matcher | Blockable | Description |
|---|---|---|---|
WorktreeCreate |
— | Yes (Exit 2) | Git worktree created. Output: path to stdout. |
WorktreeRemove |
— | No | Worktree removed. |
User Input & MCP
| Event | Matcher | Blockable | Description |
|---|---|---|---|
UserPromptSubmit |
— | Yes (Exit 2) | Before user prompt processing. |
Elicitation |
MCP server name | Yes (Exit 2) | MCP server requests input. |
ElicitationResult |
MCP server name | No | User responds to elicitation. |
Matcher Syntax
Matchers use regular expressions to filter hook events.
Matcher Support by Event:
| Event | Matcher Target | Examples |
|---|---|---|
| PreToolUse, PostToolUse, PermissionRequest | Tool name | Bash, Edit|Write, Read, mcp__.* |
| SessionStart | Session source | startup, resume, clear, compact |
| SessionEnd | Exit reason | clear, logout, other |
| SubagentStart, SubagentStop | Agent type | Explore, Plan, custom names |
| ConfigChange | Config source | user_settings, project_settings, policy_settings |
| StopFailure | Error type | rate_limit, authentication_failed, server_error |
| Elicitation, ElicitationResult | MCP server name | Configured server names |
Regex Examples:
"matcher": "Bash" // Exact: Bash only
"matcher": "Edit|Write" // Alternation: Edit or Write
"matcher": "mcp__.*" // All MCP tools
"matcher": "mcp__memory__.*" // Only memory MCP tools
"matcher": "^(Read|Grep|Glob)$" // Read-only tools
Events WITHOUT Matcher Support:
- UserPromptSubmit, Stop, TeammateIdle, TaskCompleted, WorktreeCreate, WorktreeRemove, InstructionsLoaded
Environment Variables
Always Available:
CLAUDE_PROJECT_DIR # Absolute project path
CLAUDE_SESSION_ID # Unique session ID
CLAUDE_CODE_REMOTE="true" # Set in remote environments
SessionStart Exclusive:
CLAUDE_ENV_FILE # Path to file for environment variables
# Example: echo "export NODE_ENV=production" >> "$CLAUDE_ENV_FILE"
PreToolUse Exclusive:
CLAUDE_TOOL_NAME # Name of tool being called
CLAUDE_TOOL_INPUT # JSON input of tool (as string)
HTTP Hooks:
All env vars in allowedEnvVars are interpolated in headers.
Exit Codes
| Code | Meaning | Blocking |
|---|---|---|
| 0 | Success | JSON parsed or action allowed |
| 2 | Blocking error | stderr as message, action blocked |
| 1, 3, ... | Non-blocking | stderr shown in verbose mode |
Exit Code 2 Blocking Behavior by Event:
| Event | Result |
|---|---|
| PreToolUse | Tool execution blocked |
| PermissionRequest | Permission denied |
| UserPromptSubmit | Prompt processing blocked |
| Stop, SubagentStop | Stop prevented |
| ConfigChange | Config change blocked |
| WorktreeCreate | Worktree creation blocked |
| Elicitation | Elicitation denied |
| PostToolUse, PostToolUseFailure | Non-blocking |
Configuration Format
Hooks configured in settings.json (project or user level):
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": ".claude/hooks/validate.sh",
"timeout": 30
}
]
}
],
"SessionStart": [
{
"matcher": "startup",
"hooks": [
{
"type": "command",
"command": ".claude/hooks/setup.sh",
"once": true
}
]
}
]
},
"disableAllHooks": false
}
Configuration Sources (Priority, low to high):
~/.claude/settings.json(user).claude/settings.json(project, shareable).claude/settings.local.json(project, local)- Managed policies (organization)
- Plugin
hooks/hooks.json - Skill/Agent frontmatter
Higher priority overrides lower.
Disable All Hooks:
{
"disableAllHooks": true
}
Individual hooks cannot be disabled — must be removed from config.
JSON Output Schema
Successful Response (Exit 0):
{
"continue": true,
"suppressOutput": false,
"systemMessage": "Hook executed",
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "allow"
}
}
Blocking Response (Exit 2):
{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"permissionDecisionReason": "Blocked by policy"
}
}
General Schema:
{
"continue": true|false,
"stopReason": "string (if continue=false)",
"suppressOutput": false,
"systemMessage": "string (to user)",
"hookSpecificOutput": {
"hookEventName": "EventName",
"permissionDecision": "allow|deny|ask",
"permissionDecisionReason": "string",
"updatedInput": {},
"updatedMCPToolOutput": {},
"additionalContext": "string"
}
}
HTTP Hook Error Handling
| Response | Behavior |
|---|---|
| 2xx + empty body | Success, action allowed |
| 2xx + JSON | JSON parsed as decision |
| 2xx + text | Text added as context |
| Non-2xx | Non-blocking error |
| Timeout | Non-blocking error |
To block via HTTP: Return 2xx with {"hookSpecificOutput": {"permissionDecision": "deny"}}.
Timeouts
| Hook Type | Default |
|---|---|
| Command | 600 seconds |
| Prompt | 30 seconds |
| Agent | 60 seconds |
| HTTP | 30 seconds |
| SessionEnd | 1.5 seconds (hard) |
Override SessionEnd Timeout:
CLAUDE_CODE_SESSIONEND_HOOKS_TIMEOUT_MS=5000 claude
Path Substitution
Placeholders in command:
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/script.sh"
"command": "${CLAUDE_PLUGIN_ROOT}/scripts/tool.sh"
"command": "${CLAUDE_PLUGIN_DATA}/dependencies/lib"
Always quote paths with spaces:
"command": "\"$CLAUDE_PROJECT_DIR/.claude/hooks/my script.sh\""
Async & One-Time Execution
Async Hooks (non-blocking):
{
"type": "command",
"command": ".claude/hooks/background-task.sh",
"async": true
}
Once Per Session:
{
"type": "command",
"command": ".claude/hooks/setup.sh",
"once": true
}
Hook Management
Type /hooks in Claude Code for read-only browser:
- All hook events with counts
- Source location (User, Project, Local, Plugin)
- Full handler details
Practical Examples
Block Destructive Commands
#!/bin/bash
COMMAND=$(jq -r '.tool_input.command')
if echo "$COMMAND" | grep -qE '(rm -rf|dd if=)'; then
jq -n '{hookSpecificOutput: {hookEventName: "PreToolUse", permissionDecision: "deny"}}'
exit 2
else
exit 0
fi
Set Environment Variables on Start
#!/bin/bash
if [ -n "$CLAUDE_ENV_FILE" ]; then
echo "export NODE_ENV=production" >> "$CLAUDE_ENV_FILE"
exit 0
fi
Lint After File Changes
#!/bin/bash
FILE=$(jq -r '.tool_input.file_path')
if [[ "$FILE" == *.py ]]; then
python -m pylint "$FILE"
fi
Prompt-Based Validation
{
"hooks": {
"PermissionRequest": [
{
"matcher": "Edit",
"hooks": [{
"type": "prompt",
"prompt": "Is this change safe? $ARGUMENTS",
"model": "claude-opus"
}]
}
]
}
}
Filter Secrets from Output
#!/bin/bash
jq -r '.tool_output // ""' | \
sed -E 's/password[=:]\s*[^ ]+/password=***REDACTED***/gi'
Prevent Force-Push
#!/bin/bash
COMMAND=$(jq -r '.tool_input.command')
if echo "$COMMAND" | grep -qE 'git push.*(--force|-f)'; then
jq -n '{hookSpecificOutput: {hookEventName: "PreToolUse", permissionDecision: "deny"}}'
exit 2
fi
MCP Tools Whitelist
{
"hooks": {
"PreToolUse": [
{
"matcher": "mcp__.*",
"hooks": [{
"type": "prompt",
"prompt": "Allow MCP tool? $ARGUMENTS",
"model": "claude-sonnet"
}]
}
]
}
}
Agent-Based Test Validation
{
"hooks": {
"Stop": [
{
"hooks": [{
"type": "agent",
"prompt": "Verify all tests pass and no linting errors",
"context": "fork",
"agent": "Explore"
}]
}
]
}
}
HTTP Remote Validation
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [{
"type": "http",
"url": "http://security-service:8080/validate",
"headers": {
"Authorization": "Bearer $SECURITY_TOKEN",
"X-Session-ID": "$CLAUDE_SESSION_ID"
},
"allowedEnvVars": ["SECURITY_TOKEN"],
"timeout": 10
}]
}
]
}
}
Custom Worktree (SVN)
#!/bin/bash
NAME=$(jq -r '.name')
DIR="$HOME/.claude/worktrees/$NAME"
mkdir -p "$(dirname "$DIR")"
svn checkout "https://svn.example.com/branches/$NAME" "$DIR"
echo "$DIR"
Security Notes
- Managed policy hooks cannot be disabled by users.
- Enterprises can use
allowManagedHooksOnlyto restrict hooks. - MCP elicitation hooks cannot override server schemas.
policy_settingsConfigChange blocks are ignored.
Documentation: 2026-03-21 EOFEN
