5.4 KiB
Hooks Schema
Hooks allow you to run commands at specific lifecycle events. Define them in hooks/hooks.json.
Location
hooks/hooks.json (at plugin root, referenced in plugin.json)
Structure
{
"description": "Optional description of what these hooks do",
"hooks": {
"EventName": [
{
"matcher": "pattern",
"hooks": [
{
"type": "command",
"command": "path/to/script.sh",
"timeout": 30
}
]
}
]
}
}
Event Types
PreToolUse
Runs before Claude uses a tool. Can block tool execution.
{
"PreToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PLUGIN_ROOT}/scripts/validate.sh",
"timeout": 30
}
]
}
]
}
Matcher: Regex pattern matching tool names (e.g., Write, Read, Bash.*)
Exit codes:
0: Allow (stdout visible to Claude)2: Block (stderr shown to Claude as feedback)- Other: Warning (non-blocking)
PostToolUse
Runs after a tool completes. Cannot block.
{
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PLUGIN_ROOT}/scripts/format.sh",
"timeout": 30
}
]
}
]
}
SessionStart
Runs when Claude Code session starts.
{
"SessionStart": [
{
"matcher": "startup",
"hooks": [
{
"type": "command",
"command": "echo 'Plugin loaded!'"
}
]
}
]
}
Matchers:
startup- Invoked from startupresume- Invoked from--resume,--continue, or/resumeclear- Invoked from/clearcompact- Invoked from auto or manual compact
Note: SessionStart stdout is added to context automatically for Claude.
SessionEnd
Runs when a Claude Code session ends.
{
"SessionEnd": [
{
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PLUGIN_ROOT}/scripts/cleanup.sh"
}
]
}
]
}
UserPromptSubmit
Runs when user submits a prompt. Can block prompt processing.
{
"UserPromptSubmit": [
{
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PLUGIN_ROOT}/scripts/context-injector.sh"
}
]
}
]
}
Exit codes:
0: Allow (stdout added to context)2: Block (stderr shown to user)
Stop / SubagentStop
Runs when Claude attempts to stop (main agent or subagent).
{
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PLUGIN_ROOT}/scripts/check-continuation.sh"
}
]
}
]
}
Environment Variables
Available in hook commands:
${CLAUDE_PLUGIN_ROOT}: Absolute path to plugin root${CLAUDE_PROJECT_DIR}: Project root directory (where Claude Code started)- Standard shell environment variables
Timeouts
- Default: No timeout
- Recommended: 10-30 seconds for validation
- Max: Keep under 60 seconds for good UX
Common Patterns
Validation Hook (Blocking)
{
"PreToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PLUGIN_ROOT}/scripts/validate.sh",
"timeout": 30
}
]
}
]
}
validate.sh:
#!/usr/bin/env bash
if [ validation_fails ]; then
echo "Error: validation failed" >&2
exit 2 # Block the tool
fi
exit 0 # Allow
Advanced JSON output (alternative to exit codes):
{
"permissionDecision": "deny",
"permissionDecisionReason": "File violates security policy",
"suppressOutput": true
}
Formatting Hook (Non-blocking)
{
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PLUGIN_ROOT}/scripts/format.sh",
"timeout": 30
}
]
}
]
}
Startup Message
{
"SessionStart": [
{
"matcher": "startup",
"hooks": [
{
"type": "command",
"command": "echo '✓ My Plugin loaded'"
}
]
}
]
}
Best Practices
- Use
${CLAUDE_PLUGIN_ROOT}for portable paths - Set timeouts to prevent hanging (10-30 seconds recommended)
- Exit code 2 to block (PreToolUse/UserPromptSubmit)
- Keep scripts fast (< 1 second ideally)
- Make scripts executable (
chmod +x) - Test hooks before distributing
- Handle JSON output for advanced control (see advanced examples)
Common Mistakes
❌ Absolute paths (not portable)
{
"command": "/Users/you/plugin/scripts/validate.sh"
}
✅ Plugin-relative paths
{
"command": "${CLAUDE_PLUGIN_ROOT}/scripts/validate.sh"
}
❌ No timeout on slow operations
{
"command": "npm install"
// Missing timeout!
}
✅ Set appropriate timeout
{
"command": "npm install",
"timeout": 300000
}
❌ Missing required matcher
{
"SessionStart": [
{
"hooks": [...] // No matcher!
}
]
}
✅ Include appropriate matcher
{
"SessionStart": [
{
"matcher": "startup",
"hooks": [...]
}
]
}
Debugging
Use claude --debug to see:
- Hook registration
- Hook execution timing
- Exit codes and output
- Blocking decisions