Agent skill
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.
Install this agent skill to your Project
npx add-skill https://github.com/majiayu000/claude-skill-registry/tree/main/skills/data/creating-hooks
SKILL.md
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 |
Didn't find tool you were looking for?