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.

install
source · Clone the upstream repo
git clone https://github.com/majiayu000/claude-skill-registry
Claude Code · Install into ~/.claude/skills/
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"
manifest: skills/data/creating-hooks/SKILL.md
source content

Creating Hooks

Build event-driven automation for Claude Code using hooks - scripts that execute at specific workflow points.

Quick Reference

Hook EventWhen It FiresUses MatcherCommon Use Cases
PreToolUse
Before tool executesYes (tool name)Validation, auto-approval, input modification
PostToolUse
After tool succeedsYes (tool name)Auto-formatting, linting, logging
PostToolUseFailure
After tool failsYes (tool name)Error handling, fallback logic
PermissionRequest
User shown permission dialogYes (tool name)Auto-allow/deny, policy enforcement
Notification
Claude sends notificationYes (type)Custom alerts, logging
UserPromptSubmit
User submits promptNoPrompt validation, context injection
Setup
--init
or
--maintenance
Yes (trigger)Dependency install, migrations, cleanup
Stop
Main agent finishesNoTask completion checks, force continue
SubagentStart
Subagent (Task) spawnsNoLogging, tracking, rate limiting
SubagentStop
Subagent (Task) finishesNoSubagent task validation
PreCompact
Before context compactionYes (trigger)Custom compaction handling
SessionStart
Session begins/resumesYes (source)Context loading, env setup
SessionEnd
Session endsYes (reason)Cleanup, logging

Updated for Claude Code 2.1.17

Configuration Locations

Hooks are configured in settings files (in order of precedence):

LocationScopeCommitted
~/.claude/settings.json
User (all projects)No
.claude/settings.json
ProjectYes
.claude/settings.local.json
Local projectNo
Enterprise managed policyOrganizationYes

Hook Structure

{
  "hooks": {
    "EventName": [
      {
        "matcher": "ToolPattern",
        "hooks": [
          {
            "type": "command",
            "command": "your-command-here",
            "timeout": 30
          }
        ]
      }
    ]
  }
}

Matcher Syntax

PatternMatchesExample
Write
Exact tool nameOnly Write tool
Edit|Write
Regex OREdit or Write
Notebook.*
Regex wildcardNotebookEdit, NotebookRead
mcp__memory__.*
MCP server toolsAll memory server tools
*
or
""
All toolsAny tool

Note: Matchers are case-sensitive and only apply to

PreToolUse
,
PostToolUse
, and
PermissionRequest
.

Hook Types

TypeDescriptionKey Field
command
Execute bash script
command
: bash command to run
prompt
LLM-based evaluation
prompt
: prompt text for Haiku

Hook Options

OptionTypeDescription
timeout
numberTimeout in seconds (default: 60, max: 600 as of 2.1.3)
once
booleanRun 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 CodeMeaningBehavior
0
SuccessContinue normally. stdout parsed for JSON control
2
Blocking errorBlock action. stderr shown to Claude
OtherNon-blocking errorLog warning. Continue normally

Exit Code 2 Behavior by Event

EventExit 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

VariableDescriptionAvailable In
CLAUDE_PROJECT_DIR
Absolute path to project rootAll hooks
CLAUDE_PLUGIN_ROOT
Absolute path to plugin directoryPlugin 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

  1. Create hook script

    • Write executable script (bash, python, etc.)
    • Read JSON from stdin
    • Output JSON to stdout (if needed)
    • Use appropriate exit code
  2. Configure in settings

    • Add to appropriate settings file
    • Set matcher pattern (if applicable)
    • Set timeout if needed (default: 60s)
  3. Test

    • Run
      claude --debug
      to see hook execution
    • Check
      /hooks
      menu for registration
    • Verify exit codes work as expected

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
    sudo
    commands
  • 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

HandlerDescriptionDefault Events
session-naming
Assigns human-friendly namesSessionStart
turn-tracker
Tracks turns between Stop eventsSessionStart, Stop, SubagentStop
dangerous-command-guard
Blocks dangerous Bash commandsPreToolUse
context-injection
Injects session/turn contextSessionStart, PreCompact
tool-logger
Logs tool usage with contextPostToolUse
event-logger
Logs all hook events to JSONL for indexingAll events
debug-logger
Full payload logging for debuggingAll events
metrics
Records hook execution timing metricsAll events

Environment Variables for Custom Handlers

Custom command handlers receive these environment variables:

VariableDescription
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

IssueSolution
Hook not runningCheck
/hooks
menu, verify JSON syntax
Wrong matcherTool names are case-sensitive
Command not foundUse absolute paths or
$CLAUDE_PROJECT_DIR
Script not executingCheck permissions (
chmod +x
)
Exit code ignoredOnly 0, 2, and other are recognized
Framework not loadingCheck
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

AspectSettings HooksFrontmatter Hooks
Location
settings.json
Skill/Agent/Command YAML
ScopeGlobal or projectComponent lifecycle
EventsAll 10 eventsPreToolUse, PostToolUse, Stop
CleanupManualAutomatic
once
option
NoYes

See FRONTMATTER-HOOKS.md for complete documentation.

Reference Files

FileContents
EVENTS.mdDetailed event documentation with input/output schemas
EXAMPLES.mdComplete working examples
FRONTMATTER-HOOKS.mdFrontmatter hooks in skills, agents, commands
TROUBLESHOOTING.mdCommon issues and solutions