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.
Installation
Follow the repository instructions for this skill and your agent. Review the files and dependencies before adding it to your workflow.
View source ↗GuidesSkill instructions
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 |
Examples
Read the usage examples in the source repository.
View source ↗Source history
- Indexed on
- 2026-01-20
- Index updated
- 2026-01-30
- Branch
- main
Index and sync dates describe this directory. View the source repository for code history.
GitHub ↗chat Comments (0)
Sign in to join the discussion and leave a comment.
No comments yet. Be the first to share your thoughts!