Hook Development
Before creating hooks, check latest docs:
/docs hooksor/docs hooks-guidefor current syntax and best practices.
Hooks run scripts automatically at specific points in Claude's workflow. Unlike CLAUDE.md instructions which are advisory, hooks are deterministic.
Quick Setup (Scaffolding)
# Create structure
mkdir -p .claude/hooks
# Create settings.json with a basic hook
cat > .claude/settings.json << 'EOF'
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "bash \"$CLAUDE_PROJECT_DIR\"/.claude/hooks/lint.sh",
"timeout": 30
}
]
}
]
}
}
EOF
For plugins, use hooks/hooks.json at plugin root with ${CLAUDE_PLUGIN_ROOT}.
Hook Events
| Event | When | Matcher | Can Block |
|---|---|---|---|
SessionStart |
Session begins/resumes | source¹ | No |
UserPromptSubmit |
User submits prompt | No | Yes (exit 2) |
PreToolUse |
Before tool runs | Yes | Yes (exit 2) |
PermissionRequest |
Permission dialog shown | Yes | Yes (deny) |
PostToolUse |
After tool succeeds | Yes | No |
PostToolUseFailure |
After tool fails | Yes | No |
SubagentStart |
Subagent spawns | No | No |
SubagentStop |
Subagent finishes | No | Yes (exit 2) |
Stop |
Claude finishes | No | Yes (exit 2) |
PreCompact |
Before compaction | trigger² | No |
Setup |
--init or --maintenance | trigger³ | No |
Notification |
Notification sent | type⁴ | No |
SessionEnd |
Session ends | No | No |
¹ startup, resume, clear, compact
² manual, auto
³ init, maintenance
⁴ permission_prompt, idle_prompt, auth_success
Configuration
Hooks in settings files (~/.claude/settings.json, .claude/settings.json):
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "npm run lint:fix $FILE",
"timeout": 30
}
]
}
]
}
}
Plugin hooks use ${CLAUDE_PLUGIN_ROOT} for portability.
Hook Types
Command Hooks (type: "command")
Run bash commands. Most common type.
{
"type": "command",
"command": "bash /path/to/script.sh",
"timeout": 30
}
Prompt Hooks (type: "prompt")
LLM-evaluated hooks for context-aware decisions. Best for Stop and SubagentStop.
{
"type": "prompt",
"prompt": "Evaluate if Claude should stop. Check if all tasks complete. Return {\"ok\": true} to allow, or {\"ok\": false, \"reason\": \"explanation\"} to continue.",
"timeout": 30
}
Exit Codes
| Exit | Behavior |
|---|---|
| 0 | Success - parse JSON stdout if present |
| 2 | Block action - stderr shown to Claude |
| Other | Non-blocking error - logged only |
Input (JSON via stdin)
Common fields:
{
"session_id": "abc123",
"transcript_path": "~/.claude/projects/.../session.jsonl",
"cwd": "/path/to/project",
"permission_mode": "default",
"hook_event_name": "PreToolUse"
}
Tool hooks add: tool_name, tool_input, tool_use_id
PostToolUse adds: tool_response
Output (JSON via stdout)
PreToolUse
{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "allow",
"permissionDecisionReason": "Safe operation",
"updatedInput": {"field": "modified value"},
"additionalContext": "Info for Claude"
},
"systemMessage": "User-only message"
}
permissionDecision: "allow" | "deny" | "ask"
Stop/SubagentStop (force continue)
{
"decision": "block",
"reason": "Not finished - still need to run tests"
}
SessionStart (persist env vars)
Use CLAUDE_ENV_FILE:
if [ -n "$CLAUDE_ENV_FILE" ]; then
echo 'export NODE_ENV=production' >> "$CLAUDE_ENV_FILE"
fi
Environment Variables
Always available:
CLAUDE_PROJECT_DIR- Project rootCLAUDE_CODE_REMOTE-"true"in remote environments
SessionStart/Setup only:
CLAUDE_ENV_FILE- Write env vars here to persist
Matchers
For PreToolUse, PostToolUse, PermissionRequest:
| Pattern | Matches |
|---|---|
"Write" |
Write tool only |
"Write|Edit" |
Write or Edit |
"mcp__memory__.*" |
All memory MCP tools |
".*" or "*" |
All tools (avoid) |
Case-sensitive regex.
Quick Patterns
Block dangerous commands:
#!/bin/bash
INPUT=$(cat)
CMD=$(echo "$INPUT" | jq -r '.tool_input.command // empty')
if echo "$CMD" | grep -qE 'rm -rf|git push.*--force'; then
echo "Blocked: Dangerous command" >&2
exit 2
fi
exit 0
Auto-approve safe files:
#!/bin/bash
INPUT=$(cat)
FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
if [[ "$FILE" == *.md || "$FILE" == *.txt ]]; then
echo '{"hookSpecificOutput":{"permissionDecision":"allow"}}'
fi
exit 0
Add context on prompt:
#!/bin/bash
echo "Current time: $(date)"
exit 0
Skills/Agents Hooks
Hooks in skill/agent frontmatter (scoped to that component):
---
name: my-skill
hooks:
PreToolUse:
- matcher: "Bash"
hooks:
- type: command
command: "./validate.sh"
Stop:
- hooks:
- type: prompt
prompt: "Check if all tasks complete"
---
Add once: true to run only once per session.
Debugging
- Check
/hooksto see registered hooks - Use
claude --debugfor execution details - Test commands manually first
- Verify scripts are executable
Best Practices
- Use
${CLAUDE_PROJECT_DIR}for paths - Quote all variables (
"$VAR") - Keep PreToolUse < 100ms
- Exit 0 for success, 2 to block
- Validate JSON input exists before parsing
Supporting Files
For deeper content, see:
- hooks-templates/ - Ready-to-use hook script templates
- examples/ - Pattern examples (bash, python, node)
- hooks-language-guide/ - Language-specific guides
- references/ - Best practices, troubleshooting, testing
Related Components
- Skills:
writing-skills,ecosystem-analysis - Used by agents:
hook-creator,workflow-auditor
Converted and distributed by TomeVault — claim your Tome and manage your conversions.