Claude-skill-registry claude-hooks-reference-2026
Complete reference for Claude Code hooks system (January 2026). Use when creating hooks, understanding hook events, matchers, exit codes, JSON output control, environment variables, plugin hooks, or implementing hook scripts.
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/claude-hooks-reference-2026" ~/.claude/skills/majiayu000-claude-skill-registry-claude-hooks-reference-2026 && rm -rf "$T"
skills/data/claude-hooks-reference-2026/SKILL.mdClaude Code Hooks System - Complete Reference (January 2026)
Hooks execute custom commands or prompts in response to Claude Code events. Use for automation, validation, formatting, and security.
All Hook Events
| Event | When Fired | Matcher Applies | Common Uses |
|---|---|---|---|
| Before tool execution | Yes | Validation, blocking |
| When user shown permission dialog | Yes | Auto-approval policies |
| After successful tool execution | Yes | Formatting, linting |
| After tool fails | Yes | Error handling |
| When Claude wants attention | Yes | Custom notifications |
| User submits prompt | No | Input validation |
| Claude finishes response | No | Cleanup, final checks |
| When spawning a subagent | No | Subagent initialization |
| Subagent (Task tool) completes | No | Result validation |
| Before context compaction | Yes | State backup |
| Repository setup/maintenance | Yes | One-time operations |
| Session begins or resumes | Yes | Environment setup |
| Session ends | No | Cleanup, persistence |
Configuration
Configuration Locations (Precedence highest to lowest)
- Managed -
(enterprise)managed-settings.json - Local -
(gitignored).claude/settings.local.json - Project -
(shared via git).claude/settings.json - User -
(personal)~/.claude/settings.json - Plugin -
or frontmatterhooks/hooks.json - Capability - Skill/Command/Agent frontmatter
Note: Enterprise administrators can use
allowManagedHooksOnly to block user, project, and plugin hooks.
Structure
Hooks are organized by matchers, where each matcher can have multiple hooks:
{ "hooks": { "EventName": [ { "matcher": "ToolPattern", "hooks": [ { "type": "command", "command": "your-command-here" } ] } ] } }
Fields:
- matcher: Pattern to match tool names, case-sensitive (only for
,PreToolUse
,PermissionRequest
)PostToolUse- Simple strings match exactly:
matches only the Write toolWrite - Supports regex:
orEdit|WriteNotebook.* - Use
to match all tools. Also accepts empty string (*
) or omit""matcher
- Simple strings match exactly:
- hooks: Array of hooks to execute when pattern matches
:type
for bash commands or"command"
for LLM evaluation"prompt"
: (Forcommand
) The bash command to executetype: "command"
: (Forprompt
) The prompt to send to the LLMtype: "prompt"
: (Optional) Seconds before canceling (default: 60 for commands, 30 for prompts)timeout
Project-Specific Hook Scripts
Use
$CLAUDE_PROJECT_DIR to reference scripts in your project:
{ "hooks": { "PostToolUse": [ { "matcher": "Write|Edit", "hooks": [ { "type": "command", "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/check-style.sh" } ] } ] } }
Events Without Matchers
For
UserPromptSubmit, Stop, SubagentStop, and SessionEnd, omit the matcher:
{ "hooks": { "UserPromptSubmit": [ { "hooks": [ { "type": "command", "command": "/path/to/prompt-validator.py" } ] } ] } }
Plugin Hooks
Plugins can provide hooks that integrate with user and project hooks. For complete plugin documentation including plugin.json schema, directory structure, and component integration, see ./claude-plugins-reference-2026/SKILL.md.
How Plugin Hooks Work
- Plugin hooks defined in
or custom path viahooks/hooks.json
field in plugin.jsonhooks - When plugin enabled, its hooks merge with user and project hooks
- Multiple hooks from different sources can respond to same event
- Plugin hooks run alongside custom hooks in parallel
Plugin Hook Configuration
Hooks can be configured in
hooks/hooks.json or inline in plugin.json:
{ "description": "Automatic code formatting", "hooks": { "PostToolUse": [ { "matcher": "Write|Edit", "hooks": [ { "type": "command", "command": "${CLAUDE_PLUGIN_ROOT}/scripts/format.sh", "timeout": 30 } ] } ] } }
Reference in plugin.json:
{ "name": "my-plugin", "hooks": "./hooks/hooks.json" }
Or define inline:
{ "name": "my-plugin", "hooks": { "PostToolUse": [ { "matcher": "Write|Edit", "hooks": [ { "type": "command", "command": "${CLAUDE_PLUGIN_ROOT}/scripts/format.sh" } ] } ] } }
Plugin Environment Variables
: Absolute path to the plugin directory${CLAUDE_PLUGIN_ROOT}
: Project root directory${CLAUDE_PROJECT_DIR}- All standard environment variables available
Hooks in Skills, Agents, and Slash Commands
Hooks can be defined in frontmatter. These are scoped to the component's lifecycle. For complete skill documentation, see ./claude-skills-reference-2026/SKILL.md.
Supported events:
PreToolUse, PostToolUse, Stop
Skill Example
--- name: secure-operations description: Perform operations with security checks hooks: PreToolUse: - matcher: "Bash" hooks: - type: command command: "./scripts/security-check.sh" ---
Agent Example
--- name: code-reviewer description: Review code changes hooks: PostToolUse: - matcher: "Edit|Write" hooks: - type: command command: "./scripts/run-linter.sh" ---
once
Option
onceSet
once: true to run hook only once per session. After first successful execution, hook is removed.
hooks: PreToolUse: - matcher: "Bash" hooks: - type: command command: "./scripts/one-time-setup.sh" once: true
Note: Only supported for skills and slash commands, not agents.
Event-Specific Matchers
PreToolUse / PermissionRequest / PostToolUse Common Matchers
| Matcher | Description |
|---|---|
| Subagent tasks |
| Shell commands |
| File pattern matching |
| Content search |
| File reading |
| File editing |
| File writing |
| Web fetching |
| Web search |
| NotebookEdit, NotebookRead |
| All tools from MCP server |
| MCP write tools across servers |
SessionStart Matchers
| Matcher | Trigger |
|---|---|
| New session started |
| , , or |
| command |
| Auto or manual compact |
PreCompact Matchers
| Matcher | Trigger |
|---|---|
| command |
| Auto-compact (full context) |
Setup Matchers
| Matcher | Trigger |
|---|---|
| or flags |
| flag |
Notification Matchers
| Matcher | Trigger |
|---|---|
| Permission requests from Claude |
| Claude waiting for input (60s+ idle) |
| Authentication success |
| MCP tool elicitation |
Hook Input (JSON via stdin)
Common Fields (All Events)
{ "session_id": "abc123", "transcript_path": "/path/to/session.jsonl", "cwd": "/current/working/directory", "permission_mode": "default", "hook_event_name": "PreToolUse" }
values: permission_mode
"default", "plan", "acceptEdits", "dontAsk", "bypassPermissions"
PreToolUse Input
{ "session_id": "abc123", "transcript_path": "/path/to/session.jsonl", "cwd": "/path/to/project", "permission_mode": "default", "hook_event_name": "PreToolUse", "tool_name": "Bash", "tool_input": { "command": "psql -c 'SELECT * FROM users'", "description": "Query the users table", "timeout": 120000 }, "tool_use_id": "toolu_01ABC123" }
Tool-specific
fields:tool_input
| Tool | Fields |
|---|---|
| , , , |
| , |
| , , , |
| , , |
PostToolUse Input
{ "session_id": "abc123", "transcript_path": "/path/to/session.jsonl", "cwd": "/path/to/project", "permission_mode": "default", "hook_event_name": "PostToolUse", "tool_name": "Write", "tool_input": { "file_path": "/path/to/file.txt", "content": "file content" }, "tool_response": { "filePath": "/path/to/file.txt", "success": true }, "tool_use_id": "toolu_01ABC123" }
Notification Input
{ "session_id": "abc123", "transcript_path": "/path/to/session.jsonl", "cwd": "/path/to/project", "permission_mode": "default", "hook_event_name": "Notification", "message": "Claude needs your permission to use Bash", "notification_type": "permission_prompt" }
UserPromptSubmit Input
{ "session_id": "abc123", "transcript_path": "/path/to/session.jsonl", "cwd": "/path/to/project", "permission_mode": "default", "hook_event_name": "UserPromptSubmit", "prompt": "Write a function to calculate factorial" }
Stop Input
{ "session_id": "abc123", "transcript_path": "/path/to/session.jsonl", "cwd": "/path/to/project", "permission_mode": "default", "hook_event_name": "Stop", "stop_hook_active": true }
: True when Claude is already continuing due to a stop hook. Check this to prevent infinite loops.stop_hook_active
SubagentStart Input
{ "session_id": "abc123", "transcript_path": "/path/to/session.jsonl", "cwd": "/path/to/project", "permission_mode": "default", "hook_event_name": "SubagentStart", "agent_id": "agent-abc123", "agent_type": "Explore" }
Fields:
: Unique identifier for the subagentagent_id
: Agent name (built-in like "Bash", "Explore", "Plan", or custom agent names)agent_type
SubagentStop Input
{ "session_id": "abc123", "transcript_path": "/path/to/session.jsonl", "cwd": "/path/to/project", "permission_mode": "default", "hook_event_name": "SubagentStop", "stop_hook_active": false, "agent_id": "def456", "agent_transcript_path": "/path/to/subagents/agent-def456.jsonl" }
Fields:
: Unique identifier for the subagentagent_id
: Path to the subagent's own transcript in nestedagent_transcript_path
foldersubagents/
: True when already continuing due to a stop hookstop_hook_active
PreCompact Input
{ "session_id": "abc123", "transcript_path": "/path/to/session.jsonl", "cwd": "/path/to/project", "permission_mode": "default", "hook_event_name": "PreCompact", "trigger": "manual", "custom_instructions": "" }
Setup Input
{ "session_id": "abc123", "transcript_path": "/path/to/session.jsonl", "cwd": "/path/to/project", "permission_mode": "default", "hook_event_name": "Setup", "trigger": "init" }
Fields:
: Eithertrigger
(from"init"
or--init
) or--init-only
(from"maintenance"
)--maintenance- Setup hooks have access to
for persisting environment variablesCLAUDE_ENV_FILE
SessionStart Input
{ "session_id": "abc123", "transcript_path": "/path/to/session.jsonl", "cwd": "/path/to/project", "permission_mode": "default", "hook_event_name": "SessionStart", "source": "startup", "model": "claude-sonnet-4-20250514" }
Fields:
: Indicates how session started:source
(new),"startup"
(resumed),"resume"
(after"clear"
), or/clear
(after compaction)"compact"
: Model identifier when availablemodel
: (Optional) Present when Claude Code started withagent_typeclaude --agent <name>
SessionEnd Input
{ "session_id": "abc123", "transcript_path": "/path/to/session.jsonl", "cwd": "/path/to/project", "permission_mode": "default", "hook_event_name": "SessionEnd", "reason": "exit" }
values: reason
clear, logout, prompt_input_exit, other
Hook Output
Exit Codes
| Code | Behavior |
|---|---|
| 0 | Success. stdout processed (JSON or plain text) |
| 2 | Blocking error. stderr used as error message, fed back to Claude |
| Other | Non-blocking error. stderr shown in verbose mode (Ctrl+O) |
Important: Claude Code does not see stdout if exit code is 0, except for
UserPromptSubmit and SessionStart where stdout is added to context.
Exit Code 2 Behavior Per Event
| Event | Exit Code 2 Behavior |
|---|---|
| Blocks tool call, shows stderr to Claude |
| Denies permission, shows stderr to Claude |
| Shows stderr to Claude (tool already ran) |
| Shows stderr to Claude (tool already failed) |
| Shows stderr to user only |
| Blocks prompt, erases it, shows stderr to user |
| Blocks stoppage, shows stderr to Claude |
| Shows stderr to user only |
| Blocks stoppage, shows stderr to Claude subagent |
| Shows stderr to user only |
| Shows stderr to user only |
| Shows stderr to user only |
| Shows stderr to user only |
JSON Output Control
Important: JSON output only processed with exit code 0. Exit code 2 uses stderr only.
Common JSON Fields (All Events)
{ "continue": true, "stopReason": "Message shown when continue is false", "suppressOutput": false, "systemMessage": "Optional warning message shown to user" }
| Field | Type | Effect |
|---|---|---|
| boolean | stops Claude (takes precedence over all) |
| string | Shown to user when is false |
| boolean | Hide stdout from transcript mode |
| string | Warning message shown to user |
Precedence:
continue: false takes precedence over any decision: "block" output.
PreToolUse JSON Output
{ "hookSpecificOutput": { "hookEventName": "PreToolUse", "permissionDecision": "allow", "permissionDecisionReason": "Auto-approved documentation file", "updatedInput": { "field_to_modify": "new value" }, "additionalContext": "Current environment: production. Proceed with caution." } }
| Field | Values | Effect |
|---|---|---|
| , , | Controls tool execution |
| string | Shown to user (allow/ask) or Claude (deny) |
| object | Modifies tool input before execution |
| string | Added to Claude's context |
Note:
decision and reason fields are deprecated. Use hookSpecificOutput.permissionDecision and hookSpecificOutput.permissionDecisionReason. Deprecated "approve" and "block" map to "allow" and "deny".
PermissionRequest JSON Output
Allow with modified input:
{ "hookSpecificOutput": { "hookEventName": "PermissionRequest", "decision": { "behavior": "allow", "updatedInput": { "command": "npm run lint" } } } }
Deny with message:
{ "hookSpecificOutput": { "hookEventName": "PermissionRequest", "decision": { "behavior": "deny", "message": "Command not allowed by policy", "interrupt": true } } }
PostToolUse JSON Output
{ "decision": "block", "reason": "Explanation for decision", "hookSpecificOutput": { "hookEventName": "PostToolUse", "additionalContext": "Additional information for Claude" } }
automatically prompts Claude with"block"reason
does nothing,undefined
is ignoredreason
UserPromptSubmit JSON Output
{ "decision": "block", "reason": "Prompt contains sensitive data", "hookSpecificOutput": { "hookEventName": "UserPromptSubmit", "additionalContext": "My additional context here" } }
Two ways to add context (exit code 0):
- Plain text stdout: Any non-JSON text is added as context
- JSON with
: More structured controladditionalContext
Blocking prompts:
prevents prompt processing, erases prompt from context"decision": "block"
shown to user but not added to context"reason"
Stop / SubagentStop JSON Output
{ "decision": "block", "reason": "Must be provided when Claude is blocked from stopping" }
prevents Claude from stopping. Must populate"block"reason
allows Claude to stop.undefined
is ignoredreason
Setup JSON Output
{ "hookSpecificOutput": { "hookEventName": "Setup", "additionalContext": "Repository initialized with custom configuration" } }
Note: Multiple hooks'
additionalContext values are concatenated. Setup hooks have access to CLAUDE_ENV_FILE for persisting environment variables.
SessionStart JSON Output
{ "hookSpecificOutput": { "hookEventName": "SessionStart", "additionalContext": "Project context loaded successfully" } }
Note: Multiple hooks'
additionalContext values are concatenated.
Prompt-Based Hooks
LLM-evaluated decisions using a fast model (Haiku). Also known as "agent hooks" for complex verification tasks.
How Prompt-Based Hooks Work
- Send the hook input and your prompt to Haiku
- The LLM responds with structured JSON containing a decision
- Claude Code processes the decision automatically
Configuration
{ "type": "prompt", "prompt": "Evaluate if Claude should stop: $ARGUMENTS. Check if all tasks are complete.", "timeout": 30 }
Alternatively, use
"type": "agent" for complex verification tasks that require tool access.
| Field | Required | Description |
|---|---|---|
| Yes | for LLM evaluation, for tools |
| Yes | Prompt text sent to LLM |
| No | Seconds (default: 30 for prompt, 60 for agent) |
Response Schema
The LLM must respond with JSON:
{ "ok": true, "reason": "Explanation for the decision" }
| Field | Type | Description |
|---|---|---|
| boolean | allows the action, prevents it |
| string | Required when is . Shown to Claude |
$ARGUMENTS Placeholder
Use
$ARGUMENTS in prompt to include hook input JSON. If omitted, input is appended to the prompt.
Example: Intelligent Stop Hook
{ "hooks": { "Stop": [ { "hooks": [ { "type": "prompt", "prompt": "You are evaluating whether Claude should stop working. Context: $ARGUMENTS\n\nAnalyze the conversation and determine if:\n1. All user-requested tasks are complete\n2. Any errors need to be addressed\n3. Follow-up work is needed\n\nRespond with JSON: {\"ok\": true} to allow stopping, or {\"ok\": false, \"reason\": \"your explanation\"} to continue working.", "timeout": 30 } ] } ] } }
Example: SubagentStop Validation
{ "hooks": { "SubagentStop": [ { "hooks": [ { "type": "prompt", "prompt": "Evaluate if this subagent should stop. Input: $ARGUMENTS\n\nCheck if:\n- The subagent completed its assigned task\n- Any errors occurred that need fixing\n- Additional context gathering is needed\n\nReturn: {\"ok\": true} to allow stopping, or {\"ok\": false, \"reason\": \"explanation\"} to continue." } ] } ] } }
Best Use Cases
| Event | Use Case |
|---|---|
| Intelligent task completion detection |
| Verify subagent completed task |
| Context-aware prompt validation |
| Complex permission decisions |
| Intelligent allow/deny dialogs |
Comparison with Command Hooks
| Feature | Command Hooks | Prompt Hooks |
|---|---|---|
| Execution | Runs bash script | Queries LLM |
| Decision logic | You implement in code | LLM evaluates context |
| Setup complexity | Requires script file | Configure prompt only |
| Context awareness | Limited to script | Natural language understanding |
| Performance | Fast (local) | Slower (API call) |
| Use case | Deterministic rules | Context-aware decisions |
Best Practices for Prompt Hooks
- Be specific in prompts - Clearly state what you want the LLM to evaluate
- Include decision criteria - List the factors the LLM should consider
- Test your prompts - Verify the LLM makes correct decisions for your use cases
- Set appropriate timeouts - Default is 30 seconds, adjust if needed
- Use for complex decisions - Bash hooks are better for simple, deterministic rules
Important Hook Events
Setup Hook
Runs when Claude Code is invoked with repository setup and maintenance flags (
--init, --init-only, or --maintenance).
Use Setup hooks for:
- One-time or occasional operations (dependency installation, migrations, cleanup)
- Operations you don't want on every session start
Matchers:
- Invoked frominit
or--init
flags--init-only
- Invoked frommaintenance
flag--maintenance
Key characteristics:
- Requires explicit flags because running automatically would slow down every session start
- Has access to
for persisting environment variablesCLAUDE_ENV_FILE - Output added to Claude's context
SessionStart Hook
Runs when Claude Code starts a new session or resumes an existing session.
Use SessionStart hooks for:
- Loading development context (existing issues, recent changes)
- Setting up environment variables
Important: For one-time operations like installing dependencies or running migrations, use Setup hooks instead. SessionStart runs on every session, so keep these hooks fast.
Matchers:
- New sessionsstartup
- Resumed sessions (fromresume
,--resume
, or--continue
)/resume
- Afterclear
command/clear
- After auto or manual compactcompact
PostToolUseFailure Hook
Runs immediately after a tool fails (returns an error). This complements PostToolUse, which only runs on successful tool execution.
Use PostToolUseFailure hooks for:
- Error recovery actions
- Logging tool failures
- Custom error handling and reporting
Recognizes the same matcher values as PreToolUse and PostToolUse.
SubagentStart Hook
Runs when a Claude Code subagent (Task tool call) is spawned.
Use SubagentStart hooks for:
- Subagent initialization
- Logging subagent creation
- Context injection for specific agent types
Input includes:
: Unique identifier for the subagentagent_id
: Agent name (built-in like "Bash", "Explore", "Plan", or custom agent names)agent_type
Environment Variables
| Variable | Description | Available In |
|---|---|---|
| Project root (absolute path) | All hooks |
| if remote, empty if local | All hooks |
| Path for persisting env vars | SessionStart, Setup |
| Plugin directory (absolute) | Plugin hooks |
SessionStart Environment Persistence
Example: Setting individual environment variables
#!/bin/bash if [ -n "$CLAUDE_ENV_FILE" ]; then echo 'export NODE_ENV=production' >> "$CLAUDE_ENV_FILE" echo 'export API_KEY=your-api-key' >> "$CLAUDE_ENV_FILE" echo 'export PATH="$PATH:./node_modules/.bin"' >> "$CLAUDE_ENV_FILE" fi exit 0
Example: Persisting all environment changes (e.g., nvm use)
#!/bin/bash ENV_BEFORE=$(export -p | sort) # Run setup commands that modify environment source ~/.nvm/nvm.sh nvm use 20 if [ -n "$CLAUDE_ENV_FILE" ]; then ENV_AFTER=$(export -p | sort) comm -13 <(echo "$ENV_BEFORE") <(echo "$ENV_AFTER") >> "$CLAUDE_ENV_FILE" fi exit 0
Variables in
$CLAUDE_ENV_FILE are sourced before each Bash command.
Working with MCP Tools
Claude Code hooks work with Model Context Protocol (MCP) tools.
MCP Tool Naming
MCP tools follow the pattern
mcp__<server>__<tool>:
- Memory server's create entities toolmcp__memory__create_entities
- Filesystem server's read file toolmcp__filesystem__read_file
- GitHub server's search toolmcp__github__search_repositories
Configuring Hooks for MCP Tools
{ "hooks": { "PreToolUse": [ { "matcher": "mcp__memory__.*", "hooks": [ { "type": "command", "command": "echo 'Memory operation initiated' >> ~/mcp-operations.log" } ] }, { "matcher": "mcp__.*__write.*", "hooks": [ { "type": "command", "command": "/home/user/scripts/validate-mcp-write.py" } ] } ] } }
Execution Details
| Aspect | Behavior |
|---|---|
| Timeout | 60 seconds default, configurable |
| Parallelization | All matching hooks run in parallel |
| Deduplication | Identical commands deduplicated |
| Environment | Runs in cwd with Claude Code's env |
| Hook order | Hooks from all sources execute together |
Output Handling by Event
| Event | stdout Handling |
|---|---|
| UserPromptSubmit, SessionStart, Setup | Added to Claude's context |
| PreToolUse, PostToolUse, Stop | Shown in verbose mode (Ctrl+O) |
| Notification, SessionEnd, SubagentStart | Logged to debug only () |
Security Considerations
Disclaimer
USE AT YOUR OWN RISK: Claude Code hooks execute arbitrary shell commands on your system automatically. By using hooks, you acknowledge that:
- You are solely responsible for the commands you configure
- Hooks can modify, delete, or access any files your user account can access
- Malicious or poorly written hooks can cause data loss or system damage
- Anthropic provides no warranty and assumes no liability for any damages
- You should thoroughly test hooks in a safe environment before production use
Security Best Practices
- Validate and sanitize inputs - Never trust input data blindly
- Always quote shell variables - Use
not"$VAR"$VAR - Block path traversal - Check for
in file paths.. - Use absolute paths via the $CLAUDE_PROJECT_DIR variable - Specify full paths for scripts (use
)$CLAUDE_PROJECT_DIR - Skip sensitive files - Avoid
,.env
, keys, etc..git/
Configuration Safety
Direct edits to hooks in settings files don't take effect immediately:
- Hooks snapshot captured at startup
- Snapshot used throughout the session
- Warns if hooks are modified externally
- Requires review in
menu for changes to apply/hooks
This prevents malicious hook modifications from affecting your current session.
Debugging
Enable Debug Mode
claude --debug claude --debug "hooks" # Filter to hooks only
Debug Output Example
[DEBUG] Executing hooks for PostToolUse:Write [DEBUG] Getting matching hook commands for PostToolUse with query: Write [DEBUG] Found 1 hook matchers in settings [DEBUG] Matched 1 hooks for query "Write" [DEBUG] Found 1 hook commands to execute [DEBUG] Executing hook command: <Your command> with timeout 60000ms [DEBUG] Hook command completed with status 0: <Your stdout>
Basic Troubleshooting
- Check configuration - Run
to see if your hook is registered/hooks - Verify syntax - Ensure your JSON settings are valid
- Test commands - Run hook commands manually first
- Check permissions - Make sure scripts are executable
- Review logs - Use
to see hook execution detailsclaude --debug - Validate plugin hooks - Use
orclaude plugin validate
for plugin-level hooks/plugin validate
Common Issues
| Problem | Cause | Fix |
|---|---|---|
| Hook not running | Wrong matcher pattern | Check case-sensitivity, regex |
| Command not found | Relative path | Use |
| JSON not processed | Non-zero exit code | Exit 0 for JSON processing |
| Hook times out | Slow script | Optimize or increase timeout |
| Quotes breaking | Unescaped in JSON | Use inside JSON strings |
| Plugin hook not load | Invalid plugin.json hooks config | Validate with |
| Path not found | Missing | Use variable for plugin scripts |
Validation Commands
For plugin hooks:
# CLI (from terminal) claude plugin validate . claude plugin validate ./path/to/plugin # In Claude Code session /plugin validate . /plugin validate ./path/to/plugin
For settings hooks: JSON validation with:
python3 -m json.tool .claude/settings.json
Test Commands Manually
echo '{"tool_name":"Write","tool_input":{"file_path":"test.txt"}}' | ./your-hook.sh
Code Examples
Python: Bash Command Validation (Exit Code)
#!/usr/bin/env python3 import json import re import sys # Define validation rules as (regex pattern, message) tuples VALIDATION_RULES = [ ( r"\bgrep\b(?!.*\|)", "Use 'rg' (ripgrep) instead of 'grep' for better performance", ), ( r"\bfind\s+\S+\s+-name\b", "Use 'rg --files -g pattern' instead of 'find -name'", ), ] def validate_command(command: str) -> list[str]: issues = [] for pattern, message in VALIDATION_RULES: if re.search(pattern, command): issues.append(message) return issues try: input_data = json.load(sys.stdin) except json.JSONDecodeError as e: print(f"Error: Invalid JSON input: {e}", file=sys.stderr) sys.exit(1) tool_name = input_data.get("tool_name", "") tool_input = input_data.get("tool_input", {}) command = tool_input.get("command", "") if tool_name != "Bash" or not command: sys.exit(0) issues = validate_command(command) if issues: for message in issues: print(f"• {message}", file=sys.stderr) # Exit code 2 blocks tool call and shows stderr to Claude sys.exit(2)
Python: UserPromptSubmit Context and Validation (JSON Output)
#!/usr/bin/env python3 import json import sys import re import datetime try: input_data = json.load(sys.stdin) except json.JSONDecodeError as e: print(f"Error: Invalid JSON input: {e}", file=sys.stderr) sys.exit(1) prompt = input_data.get("prompt", "") # Check for sensitive patterns sensitive_patterns = [ (r"(?i)\b(password|secret|key|token)\s*[:=]", "Prompt contains potential secrets"), ] for pattern, message in sensitive_patterns: if re.search(pattern, prompt): # Use JSON output to block with a specific reason output = { "decision": "block", "reason": f"Security policy violation: {message}. Please rephrase without sensitive information." } print(json.dumps(output)) sys.exit(0) # Add current time to context context = f"Current time: {datetime.datetime.now()}" print(context) # Equivalent JSON approach: # print(json.dumps({ # "hookSpecificOutput": { # "hookEventName": "UserPromptSubmit", # "additionalContext": context, # }, # })) sys.exit(0)
Python: PreToolUse Auto-Approval (JSON Output)
#!/usr/bin/env python3 import json import sys try: input_data = json.load(sys.stdin) except json.JSONDecodeError as e: print(f"Error: Invalid JSON input: {e}", file=sys.stderr) sys.exit(1) tool_name = input_data.get("tool_name", "") tool_input = input_data.get("tool_input", {}) # Auto-approve file reads for documentation files if tool_name == "Read": file_path = tool_input.get("file_path", "") if file_path.endswith((".md", ".mdx", ".txt", ".json")): output = { "hookSpecificOutput": { "hookEventName": "PreToolUse", "permissionDecision": "allow", "permissionDecisionReason": "Documentation file auto-approved" }, "suppressOutput": True } print(json.dumps(output)) sys.exit(0) # Let normal permission flow proceed sys.exit(0)
Node.js: SessionStart Context Injection
#!/usr/bin/env node const output = { hookSpecificOutput: { hookEventName: "SessionStart", additionalContext: `<project-context> Environment: ${process.env.NODE_ENV || "development"} Node version: ${process.version} Working directory: ${process.cwd()} </project-context>`, }, }; console.log(JSON.stringify(output));
Examples
Code Formatting (PostToolUse)
{ "hooks": { "PostToolUse": [ { "matcher": "Write|Edit", "hooks": [ { "type": "command", "command": "prettier --write \"$CLAUDE_PROJECT_DIR\"/**/*.{js,ts,json}" } ] } ] } }
File Protection (PreToolUse)
{ "hooks": { "PreToolUse": [ { "matcher": "Write|Edit", "hooks": [ { "type": "command", "command": "./scripts/check-protected-files.sh" } ] } ] } }
Custom Notifications
{ "hooks": { "Notification": [ { "matcher": "permission_prompt", "hooks": [ { "type": "command", "command": "/path/to/permission-alert.sh" } ] }, { "matcher": "idle_prompt", "hooks": [ { "type": "command", "command": "/path/to/idle-notification.sh" } ] } ] } }
Task Verification (Stop)
{ "hooks": { "Stop": [ { "hooks": [ { "type": "prompt", "prompt": "Verify task completion. Check edge cases. Return {\"ok\": true} or {\"ok\": false, \"reason\": \"...\"}." } ] } ] } }
Sources
- Hooks Reference (accessed 2026-01-28)
- Hooks Guide
- Settings Reference
- Plugin Components Reference