name: creating-hooks description: 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. allowed-tools: ["Read", "Write", "Edit", "Bash"]
Creating 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 |
|---|---|---|---|
PreToolUse | Before tool executes | Yes (tool name) | Validation, auto-approval, input modification |
PostToolUse | After tool succeeds | Yes (tool name) | Auto-formatting, linting, logging |
PostToolUseFailure | After tool fails | Yes (tool name) | Error handling, fallback logic |
PermissionRequest | User shown permission dialog | Yes (tool name) | Auto-allow/deny, policy enforcement |
Notification | Claude sends notification | Yes (type) | Custom alerts, logging |
UserPromptSubmit | User submits prompt | No | Prompt validation, context injection |
Setup | --init or --maintenance | Yes (trigger) | Dependency install, migrations, cleanup |
Stop | Main agent finishes | No | Task completion checks, force continue |
SubagentStart | Subagent (Task) spawns | No | Logging, tracking, rate limiting |
SubagentStop | Subagent (Task) finishes | No | Subagent task validation |
PreCompact | Before context compaction | Yes (trigger) | Custom compaction handling |
SessionStart | Session begins/resumes | Yes (source) | Context loading, env setup |
SessionEnd | 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 |
|---|---|---|
~/.claude/settings.json | User (all projects) | No |
.claude/settings.json | Project | Yes |
.claude/settings.local.json | 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 |
|---|---|---|
Write | Exact tool name | Only Write tool |
Edit|Write | Regex OR | Edit or Write |
Notebook.* | Regex wildcard | NotebookEdit, NotebookRead |
mcp__memory__.* | 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 |
|---|---|---|
command | Execute bash script | command: bash command to run |
prompt | LLM-based evaluation | prompt: prompt text for Haiku |
Hook Options
| Option | Type | Description |
|---|---|---|
timeout | number | Timeout in seconds (default: 60, max: 600 as of 2.1.3) |
once | 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 |
|---|---|---|
0 | Success | Continue normally. stdout parsed for JSON control |
2 | 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 |
|---|---|
PreToolUse | Blocks tool call, stderr to Claude |
PermissionRequest | Denies permission, stderr to Claude |
PostToolUse | stderr to Claude (tool already ran) |
UserPromptSubmit | Blocks prompt, erases it, stderr to user |
Stop / SubagentStop | Blocks stoppage, stderr to Claude |
Notification / SessionStart / SessionEnd / PreCompact | stderr to user only |
Environment Variables
| Variable | Description | Available In |
|---|---|---|
CLAUDE_PROJECT_DIR | Absolute path to project root | All hooks |
CLAUDE_PLUGIN_ROOT | Absolute path to plugin directory | Plugin hooks only |
CLAUDE_ENV_FILE | File path for persisting env vars | SessionStart only |
CLAUDE_CODE_REMOTE | "true" 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 PreToolUse to:
- Validate tool inputs before execution
- Auto-approve safe operations (e.g., reading docs)
- Block dangerous commands
- Modify tool inputs
After Tool Execution
Use PostToolUse to:
- Auto-format code after Write/Edit
- Run linters after file changes
- Log file modifications
- Provide feedback to Claude
Permission Automation
Use PermissionRequest to:
- Auto-allow trusted operations
- Auto-deny blocked patterns
- Enforce security policies
Prompt Processing
Use UserPromptSubmit to:
- Inject context (current time, git status)
- Validate prompts for secrets
- Block sensitive requests
Session Lifecycle
Use SessionStart to:
- Load development context
- Set environment variables
- Install dependencies
Use SessionEnd to:
- Clean up resources
- Log session statistics
Agent Completion
Use Stop / SubagentStop to:
- Verify task completion
- Force Claude to continue working
- Add completion checks
Context Management
Use PreCompact to:
- Customize compaction behavior
- Add pre-compaction context
Alerts
Use Notification to:
- 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
claude --debugto see hook execution - Check
/hooksmenu for registration - 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
rm -rf /patterns - Require approval for
sudocommands - 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 |
|---|---|---|
session-naming | Assigns human-friendly names | SessionStart |
turn-tracker | Tracks turns between Stop events | SessionStart, Stop, SubagentStop |
dangerous-command-guard | Blocks dangerous Bash commands | PreToolUse |
context-injection | Injects session/turn context | SessionStart, PreCompact |
tool-logger | Logs tool usage with context | PostToolUse |
event-logger | Logs all hook events to JSONL for indexing | All events |
debug-logger | Full payload logging for debugging | All events |
metrics | Records hook execution timing metrics | All events |
Environment Variables for Custom Handlers
Custom command handlers receive these environment variables:
| Variable | Description |
|---|---|
CLAUDE_SESSION_ID | Current session ID |
CLAUDE_SESSION_NAME | Human-friendly session name |
CLAUDE_TURN_ID | Turn identifier (session:sequence) |
CLAUDE_TURN_SEQUENCE | Current turn number |
CLAUDE_EVENT_TYPE | Hook event type |
CLAUDE_CWD | Current working directory |
CLAUDE_PROJECT_DIR | 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 /hooks menu, verify JSON syntax |
| Wrong matcher | Tool names are case-sensitive |
| Command not found | Use absolute paths or $CLAUDE_PROJECT_DIR |
| Script not executing | Check permissions (chmod +x) |
| Exit code ignored | Only 0, 2, and other are recognized |
| Framework not loading | Check hooks.yaml syntax, run with debug: true |
Run with debug mode:
claude --debug
Security Considerations
- Validate and sanitize all inputs
- Quote shell variables (
"$VAR"not$VAR) - Check for path traversal (
..) - Use absolute paths for scripts
- Skip sensitive files (
.env, keys)
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 | settings.json | Skill/Agent/Command YAML |
| Scope | Global or project | Component lifecycle |
| Events | All 10 events | PreToolUse, PostToolUse, Stop |
| Cleanup | Manual | Automatic |
once 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 |