Claude Hook Authoring
Create event hooks that automate workflows, validate operations, and respond to Claude Code events.
Hook Types
Two hook execution types:
Type Best For Example
prompt Complex reasoning, context-aware validation LLM evaluates if action is safe
command Deterministic checks, external tools, performance Bash script validates paths
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 }
Command hooks (for deterministic/fast checks):
{ "type": "command", "command": "${CLAUDE_PLUGIN_ROOT}/scripts/validate.sh", "timeout": 10 }
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
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: $TOOL_INPUT , $TOOL_RESULT , $USER_PROMPT
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_DIR or ${CLAUDE_PLUGIN_ROOT}
-
Handle errors gracefully - Use set -euo pipefail in 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
External Resources
-
Official Hooks Reference
-
Hooks Guide
-
Community Examples (disler)
-
Claude Code Showcase