| name | claude-hook-builder |
| description | Interactive hook creator for Claude Code. Triggers when user mentions creating hooks, PreToolUse, PostToolUse, hook validation, hook configuration, settings.json hooks, or wants to automate tool execution workflows. |
| allowed-tools | Read, Write, Edit, Grep, Glob, Bash |
| model | sonnet |
Claude Code Hook Builder
Purpose
Guide users through creating effective Claude Code hooks for tool validation, automation, and workflow enhancement. Auto-invokes when users want to create or configure hooks.
When to Use
Auto-invoke when users mention:
- Creating hooks - "create hook", "make hook", "new hook", "add hook"
- Hook events - "PreToolUse", "PostToolUse", "UserPromptSubmit", "Stop", "SessionStart"
- Validation - "validate", "check", "prevent", "block", "approve"
- Automation - "auto-format", "auto-lint", "automatic", "trigger"
- Hook configuration - "settings.json hooks", "hook matcher", "hook command"
Knowledge Base
- Official docs:
.claude/skills/ai/claude-code/docs/code_claude_com/docs_en_hooks.md - Hook guide:
.claude/skills/ai/claude-code/docs/code_claude_com/docs_en_hooks-guide.md - Project guide:
.claude/docs/creating-components.md
Process
1. Gather Requirements
Ask the user:
Let me help you create a Claude Code hook! I need some details:
1. **What should this hook do?**
Examples:
- Auto-format code after editing files
- Validate bash commands before execution
- Add context when user submits prompts
- Prevent access to sensitive files
- Run tests after file changes
2. **When should it trigger?**
- PreToolUse (before tool execution)
- PostToolUse (after tool execution)
- UserPromptSubmit (when user sends message)
- Stop (when Claude finishes responding)
- SubagentStop (when subagent finishes)
- SessionStart (when session begins)
- SessionEnd (when session ends)
- Notification (when notification sent)
- PermissionRequest (when permission requested)
3. **Which tools should it match?**
- Specific tool (Write, Edit, Bash, Read, etc.)
- Multiple tools (Write|Edit)
- All tools (*)
- MCP tools (mcp__server__tool)
4. **What should it return?**
- Simple exit code (0 = success, 2 = block)
- JSON with decision control
- Additional context for Claude
- Modified tool inputs
5. **Scope:**
- User-level (`~/.claude/settings.json`)
- Project-level (`.claude/settings.json`)
- Local project (`.claude/settings.local.json`)
2. Determine Hook Type
Bash Command Hook:
{
"type": "command",
"command": "/path/to/script.sh"
}
- Runs a shell command
- Fast, deterministic
- Good for validation, formatting
Prompt-based Hook:
{
"type": "prompt",
"prompt": "Evaluate if Claude should stop: $ARGUMENTS"
}
- Uses LLM for decision
- Context-aware, intelligent
- Good for complex decisions (Stop, SubagentStop)
3. Choose Hook Event
PreToolUse
Runs before tool executes.
Use for:
- Validate inputs
- Auto-approve safe operations
- Block dangerous commands
- Modify tool parameters
JSON Output:
{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "allow" | "deny" | "ask",
"permissionDecisionReason": "Why this decision",
"updatedInput": {
"field": "new value"
}
}
}
PostToolUse
Runs after tool completes.
Use for:
- Auto-format code
- Run linters
- Validate outputs
- Log operations
JSON Output:
{
"decision": "block" | undefined,
"reason": "Why blocking",
"hookSpecificOutput": {
"hookEventName": "PostToolUse",
"additionalContext": "Extra info for Claude"
}
}
UserPromptSubmit
Runs when user submits prompt.
Use for:
- Add context automatically
- Validate prompts
- Block sensitive prompts
- Inject current time/date
JSON Output:
{
"decision": "block" | undefined,
"reason": "Why blocking",
"hookSpecificOutput": {
"hookEventName": "UserPromptSubmit",
"additionalContext": "Extra context"
}
}
Stop / SubagentStop
Runs when Claude/subagent finishes.
Use for:
- Verify tasks completed
- Continue if work remains
- Intelligent stoppage control
JSON Output:
{
"decision": "block" | undefined,
"reason": "Why must continue"
}
SessionStart
Runs when session starts.
Use for:
- Load environment variables
- Set up development context
- Install dependencies
- Inject initial context
JSON Output:
{
"hookSpecificOutput": {
"hookEventName": "SessionStart",
"additionalContext": "Initial context"
}
}
Special: Can persist environment variables:
#!/bin/bash
if [ -n "$CLAUDE_ENV_FILE" ]; then
echo 'export NODE_ENV=production' >> "$CLAUDE_ENV_FILE"
fi
SessionEnd
Runs when session ends.
Use for:
- Cleanup tasks
- Save session stats
- Log session data
4. Create Hook Script
For bash command hooks, create a script:
Template:
#!/usr/bin/env bash
# Read JSON input from stdin
INPUT=$(cat)
# Parse JSON (requires jq)
TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // empty')
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
# Your validation logic here
if [[ condition ]]; then
echo "Error message" >&2
exit 2 # Block operation
fi
# Success
exit 0
Python Template:
#!/usr/bin/env python3
import json
import sys
# Read JSON input
try:
input_data = json.load(sys.stdin)
except json.JSONDecodeError as e:
print(f"Error: Invalid JSON: {e}", file=sys.stderr)
sys.exit(1)
tool_name = input_data.get("tool_name", "")
tool_input = input_data.get("tool_input", {})
# Your logic here
if condition:
# Block with error
print("Error message", file=sys.stderr)
sys.exit(2)
# Or return JSON for control
output = {
"decision": "approve",
"reason": "Auto-approved"
}
print(json.dumps(output))
sys.exit(0)
5. Configure in settings.json
Add hook configuration:
Basic Hook:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "$CLAUDE_PROJECT_DIR/.claude/hooks/format.sh"
}
]
}
]
}
}
Multiple Hooks:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "$CLAUDE_PROJECT_DIR/.claude/hooks/format.sh",
"timeout": 30
},
{
"type": "command",
"command": "$CLAUDE_PROJECT_DIR/.claude/hooks/lint.sh",
"timeout": 60
}
]
}
],
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "$CLAUDE_PROJECT_DIR/.claude/hooks/validate-bash.py"
}
]
}
]
}
}
No Matcher (events without tools):
{
"hooks": {
"UserPromptSubmit": [
{
"hooks": [
{
"type": "command",
"command": "$CLAUDE_PROJECT_DIR/.claude/hooks/add-context.sh"
}
]
}
]
}
}
6. Hook Input Reference
Each event receives JSON on stdin:
Common fields:
{
"session_id": "abc123",
"transcript_path": "/path/to/transcript.jsonl",
"cwd": "/current/working/dir",
"permission_mode": "default",
"hook_event_name": "PostToolUse"
}
PreToolUse/PostToolUse:
{
"tool_name": "Write",
"tool_input": {
"file_path": "/path/to/file.txt",
"content": "file content"
},
"tool_response": { /* PostToolUse only */
"success": true
}
}
UserPromptSubmit:
{
"prompt": "User's submitted message"
}
Stop/SubagentStop:
{
"stop_hook_active": false
}
7. Exit Codes
0: Success
- stdout shown in verbose mode (Ctrl+O)
- For UserPromptSubmit/SessionStart: stdout added to context
- JSON parsed if present
2: Blocking error
- stderr shown to Claude
- Operation blocked (behavior varies by event)
- JSON in stdout ignored
Other: Non-blocking warning
- stderr shown in verbose mode
- Execution continues
8. Test the Hook
Test script directly:
# Create test input
echo '{
"tool_name": "Write",
"tool_input": {
"file_path": "test.txt",
"content": "hello"
}
}' | .claude/hooks/your-hook.sh
# Check exit code
echo $?
Test in Claude Code:
1. Add hook to settings.json
2. Restart Claude Code
3. Run /hooks to verify it's loaded
4. Trigger the hook (e.g., write a file)
5. Check verbose mode (Ctrl+O) for output
Debug mode:
claude --debug
# Shows hook execution details
9. Provide Configuration
Show the complete configuration:
{
"hooks": {
"EventName": [
{
"matcher": "ToolPattern",
"hooks": [
{
"type": "command",
"command": "$CLAUDE_PROJECT_DIR/.claude/hooks/script.sh",
"timeout": 60
}
]
}
]
}
}
Hook Examples
Example 1: Auto-Format Python Files
Hook script (.claude/hooks/format-python.sh):
#!/usr/bin/env bash
INPUT=$(cat)
# Get file path
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
# Only process .py files
if [[ "$FILE_PATH" == *.py ]]; then
# Run black formatter
python -m black "$FILE_PATH" 2>&1
if [[ $? -eq 0 ]]; then
echo "Formatted: $FILE_PATH" >&2
fi
fi
exit 0
Configuration:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "$CLAUDE_PROJECT_DIR/.claude/hooks/format-python.sh"
}
]
}
]
}
}
Example 2: Validate Bash Commands
Hook script (.claude/hooks/validate-bash.py):
#!/usr/bin/env python3
import json
import sys
import re
# Dangerous patterns
DANGEROUS = [
(r'\brm\s+-rf\s+/', 'Dangerous: rm -rf on root'),
(r'>\s*/dev/sd[a-z]', 'Dangerous: writing to block device'),
(r'\bcurl\s+.*\|\s*bash', 'Dangerous: piping curl to bash'),
]
try:
data = json.load(sys.stdin)
except:
sys.exit(1)
if data.get('tool_name') != 'Bash':
sys.exit(0)
command = data.get('tool_input', {}).get('command', '')
# Check for dangerous patterns
for pattern, message in DANGEROUS:
if re.search(pattern, command):
print(f"⚠️ {message}", file=sys.stderr)
print(f"Command: {command}", file=sys.stderr)
sys.exit(2) # Block
sys.exit(0) # Allow
Configuration:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "$CLAUDE_PROJECT_DIR/.claude/hooks/validate-bash.py"
}
]
}
]
}
}
Example 3: Add Timestamp to Prompts
Hook script (.claude/hooks/add-timestamp.sh):
#!/usr/bin/env bash
# Output current timestamp
echo "Current time: $(date '+%Y-%m-%d %H:%M:%S %Z')"
exit 0
Configuration:
{
"hooks": {
"UserPromptSubmit": [
{
"hooks": [
{
"type": "command",
"command": "$CLAUDE_PROJECT_DIR/.claude/hooks/add-timestamp.sh"
}
]
}
]
}
}
Example 4: Auto-Approve Documentation Reads
Hook script (.claude/hooks/auto-approve-docs.py):
#!/usr/bin/env python3
import json
import sys
data = json.load(sys.stdin)
if data.get('tool_name') != 'Read':
sys.exit(0)
file_path = data.get('tool_input', {}).get('file_path', '')
# Auto-approve docs
if any(file_path.endswith(ext) for ext in ['.md', '.txt', '.json', '.yaml']):
output = {
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "allow",
"permissionDecisionReason": "Documentation file auto-approved"
},
"suppressOutput": True
}
print(json.dumps(output))
sys.exit(0)
sys.exit(0)
Configuration:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Read",
"hooks": [
{
"type": "command",
"command": "$CLAUDE_PROJECT_DIR/.claude/hooks/auto-approve-docs.py"
}
]
}
]
}
}
Example 5: Prevent Sensitive File Access
Hook script (.claude/hooks/block-secrets.sh):
#!/usr/bin/env bash
INPUT=$(cat)
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
# Block sensitive files
if [[ "$FILE_PATH" =~ \.env ||
"$FILE_PATH" =~ secrets/ ||
"$FILE_PATH" =~ \.aws/ ]]; then
echo "⛔ Access to sensitive file blocked: $FILE_PATH" >&2
exit 2
fi
exit 0
Configuration:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Read|Write|Edit",
"hooks": [
{
"type": "command",
"command": "$CLAUDE_PROJECT_DIR/.claude/hooks/block-secrets.sh"
}
]
}
]
}
}
Example 6: Intelligent Stop Hook (Prompt-based)
Configuration:
{
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "prompt",
"prompt": "Evaluate whether Claude should stop. Context: $ARGUMENTS\n\nCheck if:\n1. All tasks are complete\n2. Tests are passing\n3. No errors need addressing\n\nRespond with JSON: {\"decision\": \"approve\" or \"block\", \"reason\": \"explanation\"}",
"timeout": 30
}
]
}
]
}
}
Example 7: Session Setup Hook
Hook script (.claude/hooks/session-setup.sh):
#!/usr/bin/env bash
# Set up environment for session
if [ -n "$CLAUDE_ENV_FILE" ]; then
# Load nvm
source ~/.nvm/nvm.sh
nvm use 20
# Capture environment changes
export -p >> "$CLAUDE_ENV_FILE"
# Add custom variables
echo 'export NODE_ENV=development' >> "$CLAUDE_ENV_FILE"
fi
# Add context
echo "Development environment initialized"
echo "Node version: $(node --version)"
exit 0
Configuration:
{
"hooks": {
"SessionStart": [
{
"matcher": "startup",
"hooks": [
{
"type": "command",
"command": "$CLAUDE_PROJECT_DIR/.claude/hooks/session-setup.sh"
}
]
}
]
}
}
Matcher Patterns
Exact match:
"matcher": "Write"
Multiple tools (regex):
"matcher": "Write|Edit|NotebookEdit"
All tools:
"matcher": "*"
Or:
"matcher": ""
MCP tools:
"matcher": "mcp__github__.*"
"matcher": "mcp__.*__write.*"
Event-specific matchers:
Notification:
"matcher": "permission_prompt"
"matcher": "idle_prompt"
PreCompact:
"matcher": "manual"
"matcher": "auto"
SessionStart:
"matcher": "startup"
"matcher": "resume"
"matcher": "clear"
Environment Variables
Available in hook scripts:
$CLAUDE_PROJECT_DIR- Absolute path to project root$CLAUDE_CODE_REMOTE- "true" if remote/web, empty if local$CLAUDE_ENV_FILE- (SessionStart only) File to persist env vars- Standard environment variables
Best Practices
DO:
✅ Keep hooks fast (<100ms recommended)
✅ Provide clear error messages
✅ Use appropriate exit codes
✅ Quote variables in bash: "$VAR"
✅ Validate inputs before processing
✅ Test thoroughly before deploying
✅ Use $CLAUDE_PROJECT_DIR for portability
✅ Document what your hook does
DON'T:
❌ Run slow operations (full test suites) ❌ Block legitimate operations unnecessarily ❌ Use hooks for everything (be selective) ❌ Forget to handle errors ❌ Skip input validation ❌ Hardcode absolute paths ❌ Leave debug output in production
Security Considerations
⚠️ USE AT YOUR OWN RISK
Hooks execute arbitrary commands:
- Can modify/delete any files
- Can access sensitive data
- Can cause data loss
- Anthropic provides no warranty
Best practices:
- Validate and sanitize inputs
- Quote all variables
- Block path traversal (
..) - Use absolute paths
- Skip sensitive files
- Test in safe environment first
Troubleshooting
Hook Not Running
Check:
- Hook is in
settings.jsoncorrectly - Matcher pattern is correct (case-sensitive)
- Script has execute permissions:
chmod +x script.sh - Script shebang is correct:
#!/usr/bin/env bash - Restart Claude Code after config changes
Debug:
# Run with debug mode
claude --debug
# Check hook execution in output
# Shows: "Executing hooks for PostToolUse:Write"
Hook Errors
Check:
- Script runs standalone:
echo '{}' | ./script.sh - Exit code is correct:
echo $? - JSON output is valid:
./script.sh | jq . - Timeout is sufficient (default: 60s)
View errors:
- Verbose mode: Ctrl+O
- Debug mode:
claude --debug - Check stderr output
Permissions Issues
Check:
# Make script executable
chmod +x .claude/hooks/script.sh
# Verify permissions
ls -la .claude/hooks/
JSON Parse Errors
Validate JSON:
# Test JSON output
echo '{}' | ./script.sh | jq .
# Common issues:
# - Missing quotes
# - Trailing commas
# - Single quotes instead of double
Resources
- Official Hook Docs:
.claude/skills/ai/claude-code/docs/code_claude_com/docs_en_hooks.md - Hook Guide:
.claude/skills/ai/claude-code/docs/code_claude_com/docs_en_hooks-guide.md - Settings Reference:
.claude/skills/ai/claude-code/docs/code_claude_com/docs_en_settings.md - Project Guide:
.claude/docs/creating-components.md