Claude-skill-registry creating-hooks
Guide for implementing Claude Code hooks. Use when creating event-driven automation, auto-linting, validation, or context injection. Covers all hook events, matchers, exit codes, and environment variables.
git clone https://github.com/majiayu000/claude-skill-registry
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/creating-hooks" ~/.claude/skills/majiayu000-claude-skill-registry-creating-hooks && rm -rf "$T"
skills/data/creating-hooks/SKILL.mdCreating Hooks
Build event-driven automation for Claude Code using hooks - scripts that execute at specific workflow points.
Quick Reference
| Hook Event | When It Fires | Uses Matcher | Common Use Cases |
|---|---|---|---|
| Before tool executes | Yes (tool name) | Validation, auto-approval, input modification |
| After tool succeeds | Yes (tool name) | Auto-formatting, linting, logging |
| After tool fails | Yes (tool name) | Error handling, fallback logic |
| User shown permission dialog | Yes (tool name) | Auto-allow/deny, policy enforcement |
| Claude sends notification | Yes (type) | Custom alerts, logging |
| User submits prompt | No | Prompt validation, context injection |
| or | Yes (trigger) | Dependency install, migrations, cleanup |
| Main agent finishes | No | Task completion checks, force continue |
| Subagent (Task) spawns | No | Logging, tracking, rate limiting |
| Subagent (Task) finishes | No | Subagent task validation |
| Before context compaction | Yes (trigger) | Custom compaction handling |
| Session begins/resumes | Yes (source) | Context loading, env setup |
| Session ends | Yes (reason) | Cleanup, logging |
Updated for Claude Code 2.1.17
Configuration Locations
Hooks are configured in settings files (in order of precedence):
| Location | Scope | Committed |
|---|---|---|
| User (all projects) | No |
| Project | Yes |
| Local project | No |
| Enterprise managed policy | Organization | Yes |
Hook Structure
{ "hooks": { "EventName": [ { "matcher": "ToolPattern", "hooks": [ { "type": "command", "command": "your-command-here", "timeout": 30 } ] } ] } }
Matcher Syntax
| Pattern | Matches | Example |
|---|---|---|
| Exact tool name | Only Write tool |
| Regex OR | Edit or Write |
| Regex wildcard | NotebookEdit, NotebookRead |
| MCP server tools | All memory server tools |
or | All tools | Any tool |
Note: Matchers are case-sensitive and only apply to
PreToolUse, PostToolUse, and PermissionRequest.
Hook Types
| Type | Description | Key Field |
|---|---|---|
| Execute bash script | : bash command to run |
| LLM-based evaluation | : prompt text for Haiku |
Hook Options
| Option | Type | Description |
|---|---|---|
| number | Timeout in seconds (default: 60, max: 600 as of 2.1.3) |
| boolean | Run only once per session (frontmatter hooks only) |
Note: As of 2.1.3, the maximum hook timeout was increased from 60 seconds to 10 minutes (600s).
Exit Codes
| Exit Code | Meaning | Behavior |
|---|---|---|
| Success | Continue normally. stdout parsed for JSON control |
| Blocking error | Block action. stderr shown to Claude |
| Other | Non-blocking error | Log warning. Continue normally |
Exit Code 2 Behavior by Event
| Event | Exit Code 2 Effect |
|---|---|
| Blocks tool call, stderr to Claude |
| Denies permission, stderr to Claude |
| stderr to Claude (tool already ran) |
| Blocks prompt, erases it, stderr to user |
/ | Blocks stoppage, stderr to Claude |
/ / / | stderr to user only |
Environment Variables
| Variable | Description | Available In |
|---|---|---|
| Absolute path to project root | All hooks |
| Absolute path to plugin directory | Plugin hooks only |
| File path for persisting env vars | only |
| if running in web environment | All hooks |
Hook Input (stdin)
All hooks receive JSON via stdin with common fields:
{ "session_id": "abc123", "transcript_path": "/path/to/transcript.jsonl", "cwd": "/current/directory", "permission_mode": "default", "hook_event_name": "EventName" }
Permission modes:
default, plan, acceptEdits, dontAsk, bypassPermissions
Decision Guide: Which Hook Do I Need?
Before Tool Execution
Use
to:PreToolUse
- Validate tool inputs before execution
- Auto-approve safe operations (e.g., reading docs)
- Block dangerous commands
- Modify tool inputs
After Tool Execution
Use
to:PostToolUse
- Auto-format code after Write/Edit
- Run linters after file changes
- Log file modifications
- Provide feedback to Claude
Permission Automation
Use
to:PermissionRequest
- Auto-allow trusted operations
- Auto-deny blocked patterns
- Enforce security policies
Prompt Processing
Use
to:UserPromptSubmit
- Inject context (current time, git status)
- Validate prompts for secrets
- Block sensitive requests
Session Lifecycle
Use
to:SessionStart
- Load development context
- Set environment variables
- Install dependencies
Use
to:SessionEnd
- Clean up resources
- Log session statistics
Agent Completion
Use
/ Stop
to:SubagentStop
- Verify task completion
- Force Claude to continue working
- Add completion checks
Context Management
Use
to:PreCompact
- Customize compaction behavior
- Add pre-compaction context
Alerts
Use
to:Notification
- Custom notification routing
- Third-party integrations (Slack, Discord)
Workflow: Creating a Hook
Prerequisites
- Identify which event to hook into
- Decide: command (bash) or prompt (LLM) type
- Plan exit code behavior
Steps
-
Create hook script
- Write executable script (bash, python, etc.)
- Read JSON from stdin
- Output JSON to stdout (if needed)
- Use appropriate exit code
-
Configure in settings
- Add to appropriate settings file
- Set matcher pattern (if applicable)
- Set timeout if needed (default: 60s)
-
Test
- Run
to see hook executionclaude --debug - Check
menu for registration/hooks - Verify exit codes work as expected
- Run
Validation
- Script is executable (
)chmod +x - JSON input/output is valid
- Exit codes are correct
- Matcher pattern works
Tool-Specific Hooks
Common patterns for hooks targeting specific tools.
Bash Tool Hooks
Validate commands before execution, log sensitive operations, or block dangerous commands.
{ "hooks": { "PreToolUse": [ { "matcher": "Bash", "hooks": [ { "type": "command", "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/validate-bash.sh" } ] } ] } }
Common validations:
- Block
patternsrm -rf / - Require approval for
commandssudo - Log all commands to audit file
- Block network commands in certain contexts
Write Tool Hooks
Validate file paths, enforce naming conventions, or auto-format after write.
{ "hooks": { "PostToolUse": [ { "matcher": "Write", "hooks": [ { "type": "command", "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/after-write.sh" } ] } ] } }
Common patterns:
- Auto-format with Prettier/Black
- Validate file encoding (UTF-8)
- Check for accidental credential writes
- Run type-checking after TypeScript writes
Edit Tool Hooks
Validate edits, prevent changes to critical files, or run linting after edits.
{ "hooks": { "PreToolUse": [ { "matcher": "Edit", "hooks": [ { "type": "command", "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/validate-edit.sh" } ] } ] } }
Common patterns:
- Block edits to lock files (package-lock.json)
- Prevent edits to generated files
- Run linter after file edits
- Validate imports/exports after module changes
Read Tool Hooks
Log file access, validate read permissions, or inject context based on files read.
{ "hooks": { "PreToolUse": [ { "matcher": "Read", "hooks": [ { "type": "command", "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/validate-read.sh" } ] } ] } }
Common patterns:
- Block reading sensitive files (.env, credentials)
- Log file access for auditing
- Auto-approve reading documentation
- Inject related context when reading specific files
Common Patterns
Auto-Format on File Write
{ "hooks": { "PostToolUse": [ { "matcher": "Write|Edit", "hooks": [ { "type": "command", "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/format.sh" } ] } ] } }
Inject Context on Session Start
{ "hooks": { "SessionStart": [ { "hooks": [ { "type": "command", "command": "echo \"Git branch: $(git branch --show-current)\"" } ] } ] } }
Auto-Approve Documentation Reads
{ "hooks": { "PreToolUse": [ { "matcher": "Read", "hooks": [ { "type": "command", "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/approve-docs.py" } ] } ] } }
Hook Framework (YAML Configuration)
For projects with multiple hooks, the Hook Framework provides a YAML-based configuration with built-in handlers and environment variable injection.
Installation
bun add claude-code-sdk
YAML Configuration
Create
hooks.yaml in your project root:
version: 1 settings: debug: false parallelExecution: true defaultTimeoutMs: 30000 builtins: # Human-friendly session names (e.g., "brave-elephant") session-naming: enabled: true options: format: adjective-animal # Track turns between Stop events turn-tracker: enabled: true # Block dangerous Bash commands dangerous-command-guard: enabled: true options: blockedPatterns: - "rm -rf /" - "rm -rf ~" # Inject session context context-injection: enabled: true options: template: "Session: ${sessionName} | Turn: ${turnId}" # Log tool usage tool-logger: enabled: true options: outputPath: ~/.claude/logs/tools.log handlers: # Custom command handlers my-validator: events: [PreToolUse] matcher: "Bash" command: ./scripts/validate-command.sh timeoutMs: 5000
Built-in Handlers
| Handler | Description | Default Events |
|---|---|---|
| Assigns human-friendly names | SessionStart |
| Tracks turns between Stop events | SessionStart, Stop, SubagentStop |
| Blocks dangerous Bash commands | PreToolUse |
| Injects session/turn context | SessionStart, PreCompact |
| Logs tool usage with context | PostToolUse |
| Logs all hook events to JSONL for indexing | All events |
| Full payload logging for debugging | All events |
| Records hook execution timing metrics | All events |
Environment Variables for Custom Handlers
Custom command handlers receive these environment variables:
| Variable | Description |
|---|---|
| Current session ID |
| Human-friendly session name |
| Turn identifier (session:sequence) |
| Current turn number |
| Hook event type |
| Current working directory |
| Project root path |
TypeScript Framework
import { createFramework, handler, blockResult } from 'claude-code-sdk/hooks/framework'; const framework = createFramework({ debug: true }); // Block dangerous commands framework.onPreToolUse( handler() .id('danger-guard') .forTools('Bash') .handle(ctx => { const input = ctx.event.tool_input as { command?: string }; if (input.command?.includes('rm -rf /')) { return blockResult('Dangerous command blocked'); } return { success: true }; }) ); // Access turn/session context framework.onPostToolUse( handler() .id('context-logger') .handle(ctx => { const turnId = ctx.results.get('turn-tracker')?.data?.turnId; const sessionName = ctx.results.get('session-naming')?.data?.sessionName; console.error(`[${sessionName}] Turn ${turnId}: ${ctx.event.tool_name}`); return { success: true }; }) ); await framework.run();
Using with settings.json
Point your settings.json to the framework entry point:
{ "hooks": { "PreToolUse": [{ "matcher": "*", "hooks": [{ "type": "command", "command": "bun run hooks-framework" }] }], "PostToolUse": [{ "matcher": "*", "hooks": [{ "type": "command", "command": "bun run hooks-framework" }] }], "SessionStart": [{ "matcher": "*", "hooks": [{ "type": "command", "command": "bun run hooks-framework" }] }], "Stop": [{ "matcher": "*", "hooks": [{ "type": "command", "command": "bun run hooks-framework" }] }] } }
Debugging
| Issue | Solution |
|---|---|
| Hook not running | Check menu, verify JSON syntax |
| Wrong matcher | Tool names are case-sensitive |
| Command not found | Use absolute paths or |
| Script not executing | Check permissions () |
| Exit code ignored | Only 0, 2, and other are recognized |
| Framework not loading | Check syntax, run with |
Run with debug mode:
claude --debug
Security Considerations
- Validate and sanitize all inputs
- Quote shell variables (
not"$VAR"
)$VAR - Check for path traversal (
).. - Use absolute paths for scripts
- Skip sensitive files (
, keys).env
Frontmatter Hooks
Hooks can also be defined directly in YAML frontmatter of Skills, Agents, and Slash Commands. These hooks are:
- Lifecycle-scoped - Only active while the component executes
- Auto-cleanup - Removed when the component finishes
- Portable - Packaged with the component for distribution
Supported events:
PreToolUse, PostToolUse, Stop
Quick Example (in a Skill)
--- name: my-skill description: A skill with lifecycle hooks hooks: PreToolUse: - matcher: "Bash" hooks: - type: command command: "./validate.sh" once: true ---
Key Differences from Settings Hooks
| Aspect | Settings Hooks | Frontmatter Hooks |
|---|---|---|
| Location | | Skill/Agent/Command YAML |
| Scope | Global or project | Component lifecycle |
| Events | All 10 events | PreToolUse, PostToolUse, Stop |
| Cleanup | Manual | Automatic |
option | No | Yes |
See FRONTMATTER-HOOKS.md for complete documentation.
Reference Files
| File | Contents |
|---|---|
| EVENTS.md | Detailed event documentation with input/output schemas |
| EXAMPLES.md | Complete working examples |
| FRONTMATTER-HOOKS.md | Frontmatter hooks in skills, agents, commands |
| TROUBLESHOOTING.md | Common issues and solutions |