Expert guidance for creating Claude Code hooks. Use when configuring hooks in settings files, creating hook scripts, or implementing hook-based workflows for tool interception, validation, or automation.
Resources
1Install
npx skillscat add vitadynamics/vita-cc-market/hook-creator Install via the SkillsCat registry.
Hook Creator
This skill provides comprehensive guidance for creating and managing Claude Code hooks—powerful automation tools that intercept and respond to Claude Code events.
Quick Start
Create a simple PreToolUse hook that validates Bash commands:
# ~/.claude/settings.json or .claude/settings.json
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/validate-command.sh",
"timeout": 30
}
]
}
]
}
}#!/bin/bash
# .claude/hooks/validate-command.sh
input=$(cat)
command=$(echo "$input" | jq -r '.tool_input.command // empty')
# Block destructive commands
if [[ "$command" =~ ^rm\ -rf ]]; then
echo "Destructive command blocked" >&2
exit 2
fi
exit 0What Are Hooks?
Hooks are event-driven automation points that intercept Claude Code execution at specific moments. They receive JSON input via stdin and communicate back through exit codes and stdout.
Hook Capabilities
- Validate or block tool calls before execution (PreToolUse)
- Auto-approve permission requests (PermissionRequest)
- Run post-processing after tool execution (PostToolUse)
- Add context to conversations (UserPromptSubmit, SessionStart)
- Control session lifecycle (Stop, SubagentStop)
- Handle notifications from Claude Code (Notification)
- Integrate with MCP tools using pattern matching
Hook Types
Command hooks (type: "command"): Execute bash scripts
- Fast, deterministic, ideal for validation rules
- Have access to environment variables
- Return status via exit codes and JSON output
Prompt hooks (type: "prompt"): Use LLM for intelligent decisions
- Context-aware evaluation
- Best for complex, nuanced decisions
- Only supported for Stop and SubagentStop events
Hook Events
PreToolUse
Runs after tool parameters are created but before execution.
Common matchers: Bash, Write, Edit, Read, Task, Glob, Grep
Use cases:
- Validate or modify tool inputs before execution
- Auto-approve specific tool calls
- Block dangerous operations
Decision control:
allow: Bypass permission systemdeny: Prevent tool executionask: Show user confirmation dialog
PermissionRequest
Runs when user is shown a permission dialog.
Use cases:
- Auto-approve known-safe operations
- Deny specific permission requests
- Modify tool inputs before approval
PostToolUse
Runs immediately after successful tool completion.
Use cases:
- Trigger notifications or logging
- Validate tool outputs
- Add feedback for Claude to consider
UserPromptSubmit
Runs when user submits a prompt, before Claude processes it.
Use cases:
- Add contextual information to conversations
- Validate prompts for security issues
- Block inappropriate prompts
Stop / SubagentStop
Runs when Claude or a subagent finishes responding.
Use cases:
- Prevent premature stopping
- Ensure task completion
- Continue work automatically
SessionStart
Runs when Claude starts or resumes a session.
Matchers: startup, resume, clear, compact
Use cases:
- Load development context (recent issues, changes)
- Install dependencies or configure environment
- Persist environment variables via
$CLAUDE_ENV_FILE
SessionEnd
Runs when a Claude Code session ends.
Use cases:
- Cleanup tasks or logging
- Save session state or statistics
Notification
Runs when Claude Code sends notifications.
Matchers: permission_prompt, idle_prompt, auth_success, elicitation_dialog
Use cases:
- Custom notification handling
- Forward notifications to external systems
Configuration Structure
Basic Structure
{
"hooks": {
"EventName": [
{
"matcher": "ToolPattern|*",
"hooks": [
{
"type": "command|prompt",
"command": "bash-command",
"prompt": "llm-prompt",
"timeout": 60
}
]
}
]
}
}Configuration Locations
Hooks are configured in settings files (priority order):
~/.claude/settings.json- User settings.claude/settings.json- Project settings.claude/settings.local.json- Local project (not committed)- Plugin hooks - Auto-merged when plugins enabled
Matcher Patterns
- Exact match:
Writematches only Write tool - Multiple tools:
Edit|Writematches Edit or Write - Wildcards:
.*or*matches all tools - Empty/omitted: Matches all tools (for events without matchers)
Environment Variables
Available in all hook commands:
$CLAUDE_PROJECT_DIR: Project root directory$CLAUDE_PLUGIN_ROOT: Plugin directory (plugin hooks only)$CLAUDE_CODE_REMOTE: "true" in remote web environment (unset locally)$CLAUDE_ENV_FILE: Persist environment vars (SessionStart only)
Hook Input/Output
Input Format
Hooks receive JSON via stdin:
{
"session_id": "abc123",
"transcript_path": "/path/to/session.jsonl",
"cwd": "/current/working/directory",
"permission_mode": "default",
"hook_event_name": "PreToolUse",
"tool_name": "Bash",
"tool_input": {
"command": "echo 'hello'"
},
"tool_use_id": "toolu_01ABC123..."
}Output: Exit Codes
- 0: Success (stdout shown in verbose mode, except UserPromptSubmit/SessionStart where stdout is added as context)
- 2: Blocking error (stderr fed back to Claude, blocks action for applicable events)
- Other: Non-blocking error (stderr shown in verbose mode)
Output: JSON Control
Return structured JSON in stdout for advanced control:
{
"continue": true,
"stopReason": "Message shown when stopping",
"suppressOutput": true,
"systemMessage": "Warning to user"
}PreToolUse JSON Output
{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "allow|deny|ask",
"permissionDecisionReason": "Explanation",
"updatedInput": {
"field_to_modify": "new value"
}
}
}PostToolUse JSON Output
{
"decision": "block",
"reason": "Explanation",
"hookSpecificOutput": {
"hookEventName": "PostToolUse",
"additionalContext": "Extra information for Claude"
}
}Stop/SubagentStop JSON Output
{
"decision": "block",
"reason": "Why Claude should continue (required when blocking)"
}Common Patterns
Pattern 1: Command Validation
Validate bash commands before execution:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/validate-bash.sh",
"timeout": 30
}
]
}
]
}
}#!/bin/bash
# .claude/hooks/validate-bash.sh
input=$(cat)
command=$(echo "$input" | jq -r '.tool_input.command // empty')
# Enforce better tools
if echo "$command" | grep -qE '\bgrep\b(?!.*\|)'; then
echo "Use 'rg' (ripgrep) instead of 'grep'" >&2
exit 2
fi
if echo "$command" | grep -qE '\bfind\s+\S+\s+-name\b'; then
echo "Use 'rg --files' instead of 'find -name'" >&2
exit 2
fi
exit 0Pattern 2: Auto-Approve Safe Operations
Auto-approve documentation file reads:
{
"hooks": {
"PermissionRequest": [
{
"matcher": "Read",
"hooks": [
{
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/auto-approve-docs.sh"
}
]
}
]
}
}#!/usr/bin/env python3
# .claude/hooks/auto-approve-docs.sh
import json
import sys
input_data = json.load(sys.stdin)
file_path = input_data.get("tool_input", {}).get("file_path", "")
# Auto-approve documentation files
if file_path.endswith((".md", ".mdx", ".txt", ".json")):
output = {
"hookSpecificOutput": {
"hookEventName": "PermissionRequest",
"decision": {
"behavior": "allow"
}
},
"suppressOutput": True
}
print(json.dumps(output))
sys.exit(0)
sys.exit(0)Pattern 3: Add Session Context
Load recent changes on session start:
{
"hooks": {
"SessionStart": [
{
"matcher": "startup|resume",
"hooks": [
{
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/load-context.sh"
}
]
}
]
}
}#!/bin/bash
# .claude/hooks/load-context.sh
cd "$CLAUDE_PROJECT_DIR" || exit 0
# Get recent commits (last 5)
recent_changes=$(git log -5 --oneline --pretty=format:"- %s" 2>/dev/null)
# Get current branch
current_branch=$(git branch --show-current 2>/dev/null)
if [ -n "$recent_changes" ] || [ -n "$current_branch" ]; then
echo "# Repository Context"
echo ""
echo "**Current branch:** $current_branch"
echo ""
echo "**Recent changes:**"
echo "$recent_changes"
fi
exit 0Pattern 4: Prompt-Based Stop Hook
Use LLM to intelligently decide if Claude should stop:
{
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "prompt",
"prompt": "Analyze the conversation context: $ARGUMENTS\n\nDetermine if:\n1. All requested tasks are complete\n2. All tests pass\n3. No errors remain unaddressed\n\nRespond with JSON: {\"decision\": \"approve\"|\"block\", \"reason\": \"explanation\"}",
"timeout": 30
}
]
}
]
}
}Pattern 5: MCP Tool Integration
Target specific MCP tools:
{
"hooks": {
"PreToolUse": [
{
"matcher": "mcp__memory__.*",
"hooks": [
{
"type": "command",
"command": "echo 'Memory operation' >> ~/hooks.log"
}
]
},
{
"matcher": "mcp__.*__write.*",
"hooks": [
{
"type": "command",
"command": "/path/to/validate-mcp-write.sh"
}
]
}
]
}
}Security Considerations
Disclaimer
USE AT YOUR OWN RISK: Hooks execute arbitrary shell commands automatically. You are solely responsible for hook commands. Malicious or poorly written hooks can cause data loss or system damage.
Best Practices
- Validate inputs: Never trust input data blindly
- Quote variables: Use
"$VAR"not$VAR - Block path traversal: Check for
..in file paths - Use absolute paths: Specify full paths via
$CLAUDE_PROJECT_DIR - Skip sensitive files: Avoid
.env,.git/, keys - Test thoroughly: Test hooks in safe environment first
Configuration Safety
Direct edits to hooks don't take effect immediately. Claude Code:
- Captures hook snapshot at startup
- Uses snapshot throughout session
- Warns if hooks modified externally
- Requires review in
/hooksmenu for changes
Debugging
Basic Troubleshooting
- Check configuration: Run
/hooksto verify hook registration - Verify syntax: Ensure JSON settings are valid
- Test commands: Run hook commands manually first
- Check permissions: Ensure scripts are executable (
chmod +x) - Review logs: Use
claude --debugfor execution details
Common Issues
- Quotes not escaped: Use
\"inside JSON strings - Wrong matcher: Tool names are case-sensitive
- Command not found: Use absolute paths for scripts
- Exit code confusion: Exit code 2 blocks, others warn
Debug Output
Run claude --debug to see detailed hook execution:
[DEBUG] Executing hooks for PostToolUse:Write
[DEBUG] Found 1 hook matchers in settings
[DEBUG] Matched 1 hooks for query "Write"
[DEBUG] Executing hook command: <command> with timeout 60000ms
[DEBUG] Hook command completed with status 0Plugin Hooks
Plugins can provide hooks that integrate with user/project hooks:
{
"description": "Automatic code formatting",
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PLUGIN_ROOT}/scripts/format.sh",
"timeout": 30
}
]
}
]
}
}Place in plugins/your-plugin/hooks/hooks.json or specify custom path in plugin metadata.
Execution Details
- Timeout: 60 seconds default (configurable per command)
- Parallelization: All matching hooks run in parallel
- Deduplication: Identical hook commands auto-deduplicated
- Environment: Current directory with Claude Code's environment
- Input: JSON via stdin
- Output: Varies by event (see above)
Guidelines
- Keep hooks simple: Prefer bash scripts for straightforward logic
- Use prompt hooks sparingly: Only for nuanced, context-aware decisions
- Set appropriate timeouts: Adjust based on expected execution time
- Handle errors explicitly: Scripts should validate inputs and handle errors
- Test thoroughly: Verify hooks work as expected before production use
- Document purpose: Add comments explaining hook behavior
- Use environment variables: Leverage
$CLAUDE_PROJECT_DIRfor portability
Examples
See examples.md for complete working examples of common hook patterns.
Reference
For complete API details, see:
- official-hooks-docs.md - Full Claude Code hooks documentation
- hook-input-schemas.md - Detailed input/output schemas
- security-guide.md - Security best practices