Claude Hook Authoring
Create event hooks that automate workflows, validate operations, and respond to Claude Code events.
Hook Types
Three hook execution types:
| Type | Best For | Example | |------|----------|---------| | command | Deterministic checks, external tools, performance | Bash script validates paths | | prompt | Complex reasoning, context-aware validation | LLM evaluates if action is safe | | agent | Multi-step verification requiring tool access | Agent with Read/Grep tools verifies consistency |
Command hooks (for deterministic/fast checks):
{
"type": "command",
"command": "${CLAUDE_PLUGIN_ROOT}/scripts/validate.sh",
"timeout": 10
}
Prompt hooks (recommended for complex logic):
{
"type": "prompt",
"prompt": "Evaluate if this file write is safe: $TOOL_INPUT. Check for sensitive paths, credentials, path traversal. Return 'allow' or 'deny' with reason.",
"timeout": 30
}
Agent hooks (for complex multi-step verification):
{
"type": "agent",
"prompt": "Verify this code change maintains consistency with the existing codebase. Check imports, type signatures, and naming conventions. Use Read and Grep tools as needed.",
"allowedTools": ["Read", "Grep", "Glob"],
"timeout": 120
}
Agent hooks spawn a subagent with tool access for verification tasks that require reading files, searching code, or multi-step reasoning. Use when prompt hooks are insufficient.
Hook Events
| Event | When | Can Block | Common Uses |
|-------|------|-----------|-------------|
| PreToolUse | Before tool executes | Yes | Validate commands, check paths, enforce policies |
| PostToolUse | After tool succeeds | No | Auto-format, run linters, update docs |
| PostToolUseFailure | After tool fails | No | Error logging, retry logic, notifications |
| PermissionRequest | Permission dialog shown | Yes | Auto-allow/deny based on rules |
| UserPromptSubmit | User submits prompt | No | Add context, log activity, augment prompts |
| Notification | Claude sends notification | No | External alerts, logging |
| Stop | Main agent finishes | No | Cleanup, completion notifications |
| SubagentStart | Subagent spawns | No | Track subagent usage |
| SubagentStop | Subagent finishes | No | Log results, trigger follow-ups |
| Setup | --init, --init-only, or --maintenance flags | No | Initialize environment, install dependencies |
| PreCompact | Before context compacts | No | Backup conversation, preserve context |
| SessionStart | Session starts/resumes | No | Load context, show status, init resources |
| SessionEnd | Session ends | No | Cleanup, save state, log metrics |
See references/hook-types.md for detailed documentation of each event.
Quick Start
Auto-Format TypeScript
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit(*.ts|*.tsx)",
"hooks": [{
"type": "command",
"command": "biome check --write \"$file\"",
"timeout": 10
}]
}
]
}
}
Block Dangerous Commands
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [{
"type": "command",
"command": "$CLAUDE_PROJECT_DIR/.claude/hooks/validate-bash.sh",
"timeout": 5
}]
}
]
}
}
validate-bash.sh:
#!/usr/bin/env bash
set -euo pipefail
INPUT=$(cat)
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty')
if echo "$COMMAND" | grep -qE '\brm\s+-rf\s+/'; then
echo "Dangerous command blocked: rm -rf /" >&2
exit 2 # Exit 2 = block and show error to Claude
fi
exit 0
Smart Validation with Prompt Hook
{
"hooks": {
"PreToolUse": [
{
"matcher": "Write|Edit",
"hooks": [{
"type": "prompt",
"prompt": "Analyze this file operation for safety. Check: 1) No sensitive paths (/etc, ~/.ssh), 2) No credentials in content, 3) No path traversal (..). Tool input: $TOOL_INPUT. Respond with JSON: {\"decision\": \"allow|deny\", \"reason\": \"...\"}",
"timeout": 30
}]
}
]
}
}
Configuration Locations
| Location | Scope | Committed |
|----------|-------|-----------|
| .claude/settings.json | Project (team-shared) | Yes |
| .claude/settings.local.json | Project (local only) | No |
| ~/.claude/settings.json | Personal (all projects) | No |
| plugin/hooks/hooks.json | Plugin | Yes |
Plugin Format (hooks.json)
Uses wrapper structure:
{
"description": "Plugin hooks for auto-formatting",
"hooks": {
"PostToolUse": [...]
}
}
Settings Format (settings.json)
Direct structure (no wrapper):
{
"hooks": {
"PostToolUse": [...]
}
}
Matchers
Matchers determine which tool invocations trigger the hook. Case-sensitive.
{"matcher": "Write"} // Exact match
{"matcher": "Edit|Write"} // Multiple tools (OR)
{"matcher": "*"} // All tools
{"matcher": "Write(*.py)"} // File pattern
{"matcher": "Write|Edit(*.ts|*.tsx)"} // Multiple + pattern
{"matcher": "mcp__memory__.*"} // MCP server tools
{"matcher": "mcp__github__create_issue"} // Specific MCP tool
Lifecycle hooks (SessionStart, SessionEnd, Stop, Notification) use special matchers:
// SessionStart matchers
{"matcher": "startup"} // Initial start
{"matcher": "resume"} // --resume or --continue
{"matcher": "clear"} // After /clear
{"matcher": "compact"} // After compaction
// PreCompact matchers
{"matcher": "manual"} // User triggered /compact
{"matcher": "auto"} // Automatic compaction
See references/matchers.md for advanced patterns.
Input Format
All hooks receive JSON on stdin:
{
"session_id": "abc123",
"transcript_path": "/path/to/transcript.jsonl",
"cwd": "/current/working/directory",
"hook_event_name": "PreToolUse",
"permission_mode": "ask",
"tool_name": "Write",
"tool_input": {
"file_path": "/project/src/file.ts",
"content": "export const foo = 'bar';"
}
}
Event-specific fields:
- Tool hooks:
tool_name,tool_input,tool_result(PostToolUse) - UserPromptSubmit:
user_prompt - Stop/SubagentStop:
reason
Prompt hooks access fields via placeholders:
$ARGUMENTS- Full context passed to the hook (general-purpose)$TOOL_INPUT- Tool input for tool-related events$TOOL_RESULT- Tool result (PostToolUse only)$USER_PROMPT- User prompt (UserPromptSubmit only)
Reading Input
Bash:
#!/usr/bin/env bash
set -euo pipefail
INPUT=$(cat)
TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name')
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
Bun/TypeScript:
#!/usr/bin/env bun
const input = await Bun.stdin.json();
const toolName = input.tool_name;
const filePath = input.tool_input?.file_path;
Output Format
Exit Codes (Simple)
exit 0 # Success, continue execution
exit 2 # Block operation (PreToolUse only), stderr shown to Claude
exit 1 # Warning, stderr shown to user, continues
JSON Output (Advanced)
{
"continue": true,
"suppressOutput": false,
"systemMessage": "Context for Claude",
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "allow|deny|ask",
"permissionDecisionReason": "Explanation",
"updatedInput": {"modified": "field"}
}
}
PreToolUse can modify tool input via updatedInput and control permissions via permissionDecision.
Environment Variables
| Variable | Availability | Description |
|----------|--------------|-------------|
| $CLAUDE_PROJECT_DIR | All hooks | Project root directory |
| $CLAUDE_PLUGIN_ROOT | Plugin hooks | Plugin root (use for portable paths) |
| $file | PostToolUse (Write/Edit) | Path to affected file |
| $CLAUDE_ENV_FILE | SessionStart | Write env vars here to persist |
| $CLAUDE_CODE_REMOTE | All hooks | Set if running in remote context |
Plugin hooks should always use ${CLAUDE_PLUGIN_ROOT} for portability:
{
"command": "${CLAUDE_PLUGIN_ROOT}/scripts/validate.sh"
}
SessionStart can persist environment variables:
#!/usr/bin/env bash
# Persist variables for the session
echo "export PROJECT_TYPE=nodejs" >> "$CLAUDE_ENV_FILE"
echo "export API_URL=https://api.example.com" >> "$CLAUDE_ENV_FILE"
Component-Scoped Hooks
Skills, agents, and commands can define hooks in frontmatter. These hooks only run when the component is active.
Supported events: PreToolUse, PostToolUse, Stop
Skill with Hooks
---
name: my-skill
description: Skill with validation hooks
hooks:
PreToolUse:
- matcher: "Write|Edit"
hooks:
- type: prompt
prompt: "Validate this write operation for the skill context..."
---
Agent with Hooks
---
name: security-reviewer
model: sonnet
hooks:
PreToolUse:
- matcher: "Bash"
hooks:
- type: command
command: "${CLAUDE_PLUGIN_ROOT}/scripts/validate-bash.sh"
Stop:
- matcher: "*"
hooks:
- type: prompt
prompt: "Verify the security review is complete..."
---
Execution Model
Parallel execution: All matching hooks run in parallel, not sequentially.
{
"PreToolUse": [{
"matcher": "Write",
"hooks": [
{"type": "command", "command": "check1.sh"}, // Runs in parallel
{"type": "command", "command": "check2.sh"}, // Runs in parallel
{"type": "prompt", "prompt": "Validate..."} // Runs in parallel
]
}]
}
Implications:
- Hooks cannot see each other's output
- Non-deterministic ordering
- Design for independence
Hot-swap limitations: Hook changes require restarting Claude Code. Editing hooks.json or hook scripts does not affect the current session.
Security Best Practices
- Validate all input - Check for path traversal, sensitive paths, injection
- Quote shell variables - Always use
"$VAR"not$VAR - Set timeouts - Prevent hanging hooks (default: 60s command, 30s prompt)
- Use absolute paths - Via
$CLAUDE_PROJECT_DIRor${CLAUDE_PLUGIN_ROOT} - Handle errors gracefully - Use
set -euo pipefailin bash - Don't log sensitive data - Filter credentials, tokens, API keys
See references/security.md for detailed security patterns.
Debugging
# Run Claude with debug output
claude --debug
# Test hook manually
echo '{"tool_name": "Write", "tool_input": {"file_path": "test.ts"}}' | ./.claude/hooks/my-hook.sh
# Check transcript for hook execution
# Press Ctrl+R in Claude Code to view transcript
Common issues:
- Hook not firing: Check matcher syntax, restart Claude Code
- Permission errors:
chmod +x script.sh - Timeout: Increase timeout value or optimize script
Workflow Patterns
Pre-Commit Quality Gate
{
"hooks": {
"PreToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{"type": "command", "command": "./.claude/hooks/validate-paths.sh"},
{"type": "command", "command": "./.claude/hooks/check-sensitive.sh"}
]
}
],
"PostToolUse": [
{
"matcher": "Write|Edit(*.ts)",
"hooks": [
{"type": "command", "command": "biome check --write \"$file\""},
{"type": "command", "command": "tsc --noEmit \"$file\""}
]
}
]
}
}
Context Injection
{
"hooks": {
"SessionStart": [{
"matcher": "startup",
"hooks": [{
"type": "command",
"command": "echo \"Branch: $(git branch --show-current)\" && git status --short"
}]
}],
"UserPromptSubmit": [{
"matcher": "*",
"hooks": [{
"type": "command",
"command": "echo \"Time: $(date '+%Y-%m-%d %H:%M %Z')\""
}]
}]
}
}
References
- references/hook-types.md - Detailed documentation for each hook event
- references/matchers.md - Advanced matcher patterns and MCP tools
- references/security.md - Security best practices and validation patterns
- references/schema.md - Complete configuration schema reference
- references/examples.md - Real-world hook implementations