claude-hook-authoring
Creates event hooks for Claude Code automation with proper configuration, matchers, input/output handling, and security best practices. Covers all 9 hook types (PreToolUse, PostToolUse, UserPromptSubmit, Notification, Stop, SubagentStop, PreCompact, SessionStart, SessionEnd). Use when building automation, creating hooks, setting up event handlers, or when users mention hooks, automation, event handlers, or tool interception.
$ 설치
git clone https://github.com/outfitter-dev/agents /tmp/agents && cp -r /tmp/agents/agent-kit/skills/claude-hook-authoring ~/.claude/skills/agents// tip: Run this command in your terminal to install the skill
name: claude-hook-authoring description: Creates event hooks for Claude Code automation with proper configuration, matchers, input/output handling, and security best practices. Covers all 9 hook types (PreToolUse, PostToolUse, UserPromptSubmit, Notification, Stop, SubagentStop, PreCompact, SessionStart, SessionEnd). Use when building automation, creating hooks, setting up event handlers, or when users mention hooks, automation, event handlers, or tool interception. version: 1.0.0
Claude Hook Authoring
Create event hooks that automate workflows and respond to Claude Code events.
Overview
Event hooks are shell commands or scripts that run automatically in response to Claude Code events:
- PreToolUse: Before a tool executes (can block/approve)
- PostToolUse: After a tool completes
- UserPromptSubmit: When user submits a prompt
- Notification: When Claude sends notifications
- Stop: When main agent finishes
- SubagentStop: When subagent finishes
- PreCompact: Before conversation compacts
- SessionStart: When session starts/resumes
- SessionEnd: When session ends
Quick Start
Basic PostToolUse Hook
Auto-format Python files after writing:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write(*.py)",
"hooks": [
{
"type": "command",
"command": "black \"$file\""
}
]
}
]
}
}
Add to .claude/settings.json or ~/.claude/settings.json.
Validation Hook
Prevent problematic bash commands:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "$CLAUDE_PROJECT_DIR/.claude/hooks/validate-bash.sh",
"timeout": 5
}
]
}
]
}
}
validate-bash.sh:
#!/usr/bin/env bash
set -euo pipefail
# Read hook input from stdin
INPUT=$(cat)
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty')
# Validate command
if echo "$COMMAND" | grep -qE '\brm\s+-rf\s+/'; then
echo "❌ Dangerous command blocked: rm -rf /" >&2
exit 2 # Exit 2 = block and show error to Claude
fi
# Approve
exit 0
Hook Configuration
Structure
{
"hooks": {
"<EventName>": [
{
"matcher": "<ToolPattern>",
"hooks": [
{
"type": "command",
"command": "<shell-command>",
"timeout": 30
}
]
}
]
}
}
Location
Project hooks (.claude/settings.json):
{
"hooks": {
"PostToolUse": [...]
}
}
Personal hooks (~/.claude/settings.json):
{
"hooks": {
"PostToolUse": [...]
}
}
Plugin hooks (plugin/hooks/hooks.json or plugin/.claude-plugin/plugin.json):
{
"hooks": {
"PostToolUse": [...]
}
}
Matchers
Matchers determine which tool invocations trigger the hook.
Simple Matchers
{"matcher": "Write"} // Only Write tool
{"matcher": "Edit"} // Only Edit tool
{"matcher": "Bash"} // Only Bash tool
Regex Matchers
{"matcher": "Edit|Write"} // Edit OR Write
{"matcher": "Notebook.*"} // Any Notebook tool
{"matcher": "Write|Edit|Notebook.*"} // Multiple tools
Wildcard Matcher
{"matcher": "*"} // ALL tools
File Pattern Matchers
{"matcher": "Write(*.py)"} // Write Python files
{"matcher": "Edit(*.ts)"} // Edit TypeScript files
{"matcher": "Write(*.md)"} // Write Markdown files
{"matcher": "Write|Edit(*.js)"} // Write or Edit JS files
MCP Tool Matchers
{"matcher": "mcp__memory__.*"} // Any memory MCP tool
{"matcher": "mcp__github__.*"} // Any GitHub MCP tool
{"matcher": "mcp__.*__.*"} // Any MCP tool
Hook Input
Hooks receive JSON on stdin with event context:
{
"session_id": "abc123",
"transcript_path": "/path/to/transcript.jsonl",
"cwd": "/current/working/directory",
"hook_event_name": "PreToolUse",
"tool_name": "Write",
"tool_input": {
"file_path": "/path/to/file.txt",
"content": "file content"
}
}
Reading Input in Bash
#!/usr/bin/env bash
set -euo pipefail
# Read and parse JSON input
INPUT=$(cat)
TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name')
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
# Use the data
echo "Tool: $TOOL_NAME"
echo "File: $FILE_PATH"
Reading Input in Bun/TypeScript
#!/usr/bin/env bun
import { stdin } from "process";
// Read stdin
const chunks: Buffer[] = [];
for await (const chunk of stdin) {
chunks.push(chunk);
}
const input = JSON.parse(Buffer.concat(chunks).toString());
// Access data
const toolName = input.tool_name;
const filePath = input.tool_input?.file_path;
console.log(`Tool: ${toolName}`);
console.log(`File: ${filePath}`);
Hook Output
Exit Codes (Simple)
#!/usr/bin/env bash
# Success (continue)
exit 0
# Blocking error (show to Claude)
echo "Error: validation failed" >&2
exit 2
# Non-blocking error (show to user)
echo "Warning: check failed" >&2
exit 1
Behavior:
- Exit 0: Success, stdout shown to user
- Exit 2: Blocking error, stderr shown to Claude
- Other: Non-blocking error, stderr shown to user
JSON Output (Advanced)
{
"continue": true,
"stopReason": "Optional message",
"suppressOutput": false,
"systemMessage": "Warning message",
"decision": "block",
"reason": "Explanation",
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"permissionDecisionReason": "Dangerous operation",
"additionalContext": "Context for Claude"
}
}
Hook Types
PreToolUse
Runs before tool executes. Can block or approve operations.
Common uses:
- Validate bash commands
- Check file paths
- Enforce security policies
- Add context before execution
{
"hooks": {
"PreToolUse": [
{
"matcher": "Write|Edit",
"hooks": [{
"type": "command",
"command": "./.claude/hooks/check-file-policy.sh"
}]
}
]
}
}
PostToolUse
Runs after tool completes successfully.
Common uses:
- Auto-format code
- Update documentation
- Run linters
- Trigger builds
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit(*.ts)",
"hooks": [{
"type": "command",
"command": "biome check --write \"$file\""
}]
}
]
}
}
UserPromptSubmit
Runs when user submits a prompt.
Common uses:
- Add current time/date
- Add environment context
- Log user activity
- Pre-process prompts
{
"hooks": {
"UserPromptSubmit": [
{
"matcher": "*",
"hooks": [{
"type": "command",
"command": "./.claude/hooks/add-context.sh"
}]
}
]
}
}
Notification
Runs when Claude sends notifications.
Common uses:
- Send to external systems
- Log notifications
- Trigger alerts
{
"hooks": {
"Notification": [
{
"matcher": "*",
"hooks": [{
"type": "command",
"command": "./.claude/hooks/log-notification.sh"
}]
}
]
}
}
Stop
Runs when main Claude agent finishes responding.
Common uses:
- Clean up resources
- Send completion notifications
- Update external systems
{
"hooks": {
"Stop": [
{
"matcher": "*",
"hooks": [{
"type": "command",
"command": "./.claude/hooks/on-completion.sh"
}]
}
]
}
}
SubagentStop
Runs when a subagent finishes.
Common uses:
- Track subagent usage
- Log subagent results
- Trigger follow-up actions
{
"hooks": {
"SubagentStop": [
{
"matcher": "*",
"hooks": [{
"type": "command",
"command": "./.claude/hooks/on-subagent-done.sh"
}]
}
]
}
}
PreCompact
Runs before conversation compacts.
Matchers:
manual: User-triggered (/compact)auto: Automatic compact
{
"hooks": {
"PreCompact": [
{
"matcher": "manual",
"hooks": [{
"type": "command",
"command": "./.claude/hooks/before-compact.sh"
}]
}
]
}
}
SessionStart
Runs when session starts or resumes.
Matchers:
startup: Claude Code startsresume: Session resumes (--resume)clear: After/clearcompact: After compact
{
"hooks": {
"SessionStart": [
{
"matcher": "startup",
"hooks": [{
"type": "command",
"command": "echo 'Welcome!' && git status"
}]
}
]
}
}
SessionEnd
Runs when session ends.
Reasons:
clear: User ran/clearlogout: User logged outprompt_input_exit: Exited during promptother: Other reasons
{
"hooks": {
"SessionEnd": [
{
"matcher": "*",
"hooks": [{
"type": "command",
"command": "./.claude/hooks/cleanup.sh"
}]
}
]
}
}
Security Best Practices
1. Validate All Input
#!/usr/bin/env bash
set -euo pipefail
INPUT=$(cat)
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
# Check for path traversal
if echo "$FILE_PATH" | grep -q '\.\.'; then
echo "❌ Path traversal detected" >&2
exit 2
fi
# Check for sensitive paths
if echo "$FILE_PATH" | grep -qE '^/etc/|^/root/|\.env$'; then
echo "❌ Sensitive path blocked" >&2
exit 2
fi
2. Quote Shell Variables
# ❌ WRONG - vulnerable to injection
rm $FILE_PATH
# ✅ CORRECT - properly quoted
rm "$FILE_PATH"
3. Use Absolute Paths
{
"hooks": {
"PostToolUse": [{
"hooks": [{
"type": "command",
"command": "$CLAUDE_PROJECT_DIR/.claude/hooks/format.sh"
}]
}]
}
}
4. Set Timeouts
{
"hooks": [{
"type": "command",
"command": "./slow-operation.sh",
"timeout": 30
}]
}
5. Handle Errors Gracefully
#!/usr/bin/env bash
set -euo pipefail
# Always validate input exists
if ! command -v jq &>/dev/null; then
echo "Error: jq not installed" >&2
exit 1
fi
# Catch errors
if ! INPUT=$(cat 2>&1); then
echo "Error: Failed to read stdin" >&2
exit 1
fi
Common Patterns
See REFERENCE.md for:
- Advanced hook configurations
- Complex matchers
- Hook chaining
- Error handling patterns
- Integration examples
See EXAMPLES.md for:
- Real-world hook implementations
- Auto-formatting workflows
- Validation patterns
- CI/CD integration
- Team workflows
Utilities
# Create hook with template
./scripts/scaffold-hook.sh format-typescript
# Validate hook configuration
./scripts/validate-hook.sh .claude/settings.json
# Test hook script with sample input
./scripts/test-hook.ts .claude/hooks/my-hook.sh
Debugging Hooks
Enable Debug Mode
claude --debug
Check Hook Output
Use transcript mode (Ctrl+R) to see hook execution and output.
Test Hook Manually
# Create sample input
cat > /tmp/hook-input.json << 'EOF'
{
"tool_name": "Write",
"tool_input": {
"file_path": "test.ts",
"content": "console.log('test');"
}
}
EOF
# Test hook
cat /tmp/hook-input.json | ./.claude/hooks/my-hook.sh
Common Issues
Hook not firing:
- Check matcher syntax
- Verify hook is in correct settings file
- Restart Claude Code
Permission errors:
- Make script executable:
chmod +x script.sh - Check file paths are correct
- Verify
$CLAUDE_PROJECT_DIRis set
Timeout errors:
- Increase timeout value
- Optimize script performance
- Check for hanging commands
Environment Variables
Available in hook scripts:
$CLAUDE_PROJECT_DIR: Project root directory$file: File path (PostToolUse hooks)- Custom variables from settings.json
Related Skills
- claude-command-authoring: Combine commands with hooks
- claude-plugin-authoring: Package hooks into plugins
- claude-config-management: Manage hook configuration
Repository
