Creating Claude Code Hooks
Use this skill when creating, improving, or publishing Claude Code hooks. Provides essential guidance on hook format, event handling, I/O conventions, and package structure.
When to Use This Skill
Activate this skill when:
-
User asks to create a new Claude Code hook
-
User wants to publish a hook as a PRPM package
-
User needs to understand hook format or events
-
User is troubleshooting hook execution
-
User asks about hook vs skill vs command differences
Quick Reference
Hook File Format
Aspect Requirement
Location .claude/hooks/<event-name>
Format Executable file (shell, TypeScript, Python, etc.)
Permissions Must be executable (chmod +x )
Shebang Required (#!/bin/bash or #!/usr/bin/env node )
Input JSON via stdin
Output Text via stdout (shown to user)
Exit Codes 0 = success, 2 = block, other = error
Available Events
Event When It Fires Common Use Cases
session-start
New session begins Environment setup, logging, checks
user-prompt-submit
Before user input processes Validation, enhancement, filtering
tool-call
Before tool execution Permission checks, logging, modification
assistant-response
After assistant responds Formatting, logging, cleanup
Hook Format Requirements
File Location
Project hooks:
.claude/hooks/session-start .claude/hooks/user-prompt-submit
User-global hooks:
~/.claude/hooks/session-start ~/.claude/hooks/tool-call
Executable Requirements
Every hook MUST:
- Have a shebang line:
#!/bin/bash
or
#!/usr/bin/env node
or
#!/usr/bin/env python3
- Be executable:
chmod +x .claude/hooks/session-start
- Handle JSON input from stdin:
#!/bin/bash INPUT=$(cat) FILE=$(echo "$INPUT" | jq -r '.input.file_path // empty')
- Exit with appropriate code:
exit 0 # Success exit 2 # Block operation exit 1 # Error (logs but continues)
Input/Output Format
JSON Input Structure
Hooks receive JSON via stdin with event-specific data:
{ "event": "tool-call", "timestamp": "2025-01-15T10:30:00Z", "session_id": "abc123", "current_dir": "/path/to/project", "input": { "file_path": "/path/to/file.ts", "command": "npm test", "old_string": "...", "new_string": "..." } }
Stdout Output
-
Normal output shows in transcript
-
Empty output runs silently
-
Use stderr (>&2 ) for errors
Exit Codes
Code Meaning Behavior
0
Success Continue normally
2
Block Stop operation, show error
1 or other Error Log error, continue
Schema Validation
Hooks should validate against the JSON schema:
Schema URL: https://github.com/pr-pm/prpm/blob/main/packages/converters/schemas/claude-hook.schema.json
Required frontmatter fields:
-
name
-
Hook identifier (lowercase, hyphens only)
-
description
-
What the hook does
-
event
-
Event type (optional, inferred from filename)
-
language
-
bash, typescript, javascript, python, binary (optional)
-
hookType: "hook"
-
For round-trip conversion
Common Mistakes
Mistake Problem Solution
Not quoting variables Breaks on spaces Always use "$VAR"
Missing shebang Won't execute Add #!/bin/bash
Not executable Permission denied Run chmod +x hook-file
Logging to stdout Clutters transcript Use stderr: echo "log" >&2
Wrong exit code Doesn't block when needed Use exit 2 to block
No input validation Security risk Always validate JSON fields
Slow operations Blocks Claude Run in background or use PostToolUse
Absolute paths missing Can't find scripts Use $CLAUDE_PLUGIN_ROOT
Basic Hook Examples
Shell Script Hook
#!/bin/bash
.claude/hooks/session-start
Log session start
echo "Session started at $(date)" >> ~/.claude/session.log
Check environment
if ! command -v node &> /dev/null; then echo "Warning: Node.js not installed" >&2 fi
Output to user
echo "Development environment ready" exit 0
TypeScript Hook
#!/usr/bin/env node // .claude/hooks/user-prompt-submit
import { readFileSync } from 'fs';
// Read JSON from stdin const input = readFileSync(0, 'utf-8'); const data = JSON.parse(input);
// Validate prompt if (data.prompt.includes('API_KEY')) { console.error('Warning: Prompt may contain secrets'); process.exit(2); // Block }
console.log('Prompt validated'); process.exit(0);
Best Practices
- Keep Hooks Fast
Target < 100ms for PreToolUse hooks:
-
Cache results where possible
-
Run heavy operations in background
-
Use specific matchers, not wildcards
- Handle Errors Gracefully
Check dependencies exist
if ! command -v jq &> /dev/null; then echo "jq not installed, skipping" >&2 exit 0 fi
Validate input
FILE=$(echo "$INPUT" | jq -r '.input.file_path // empty') if [[ -z "$FILE" ]]; then echo "No file path provided" >&2 exit 1 fi
- Use Shebangs
Always start with shebang:
#!/bin/bash #!/usr/bin/env node #!/usr/bin/env python3
- Secure Sensitive Files
BLOCKED=(".env" ".env." ".pem" "*.key") for pattern in "${BLOCKED[@]}"; do case "$FILE" in $pattern) echo "Blocked: $FILE is sensitive" >&2 exit 2 ;; esac done
- Quote All Variables
WRONG - breaks on spaces
prettier --write $FILE
RIGHT - handles spaces
prettier --write "$FILE"
- Log for Debugging
LOG_FILE=~/.claude-hooks/debug.log
Log to file
echo "[$(date)] Processing $FILE" >> "$LOG_FILE"
Log to stderr (shows in transcript)
echo "Hook running..." >&2
Publishing as PRPM Package
Package Structure
my-hook/ ├── prpm.json # Package manifest ├── HOOK.md # Hook documentation └── hook-script.sh # Hook executable
prpm.json
{ "name": "@username/hook-name", "version": "1.0.0", "description": "Brief description shown in search", "author": "Your Name", "format": "claude", "subtype": "hook", "tags": ["automation", "security", "formatting"], "main": "HOOK.md" }
HOOK.md Format
name: session-logger description: Logs session start/end times for tracking event: SessionStart language: bash hookType: hook
Session Logger Hook
Logs Claude Code session activity for tracking and debugging.
Installation
This hook will be installed to .claude/hooks/session-start.
Behavior
- Logs session start time to
~/.claude/session.log - Displays environment status
- Runs silent dependency checks
Requirements
- bash 4.0+
- write access to
~/.claude/
Source Code
```bash #!/bin/bash echo "Session started at $(date)" >> ~/.claude/session.log echo "Environment ready" exit 0 ```
Publishing Process
Test locally first
prpm test
Publish to registry
prpm publish
Version bumps
prpm publish patch # 1.0.0 -> 1.0.1 prpm publish minor # 1.0.0 -> 1.1.0 prpm publish major # 1.0.0 -> 2.0.0
Security Requirements
Input Validation
Parse JSON safely
INPUT=$(cat) if ! FILE=$(echo "$INPUT" | jq -r '.input.file_path // empty' 2>&1); then echo "JSON parse failed" >&2 exit 1 fi
Validate field exists
[[ -n "$FILE" ]] || exit 1
Path Sanitization
Prevent directory traversal
if [[ "$FILE" == ".." ]]; then echo "Path traversal detected" >&2 exit 2 fi
Keep in project directory
if [[ "$FILE" != "$CLAUDE_PROJECT_DIR"* ]]; then echo "File outside project" >&2 exit 2 fi
User Confirmation
Claude Code automatically:
-
Requires confirmation before installing hooks
-
Shows hook source code to user
-
Warns about hook execution
-
Displays hook output in transcript
Hooks vs Skills vs Commands
Feature Hooks Skills Commands
Format Executable code Markdown Markdown
Trigger Automatic (events) Automatic (context) Manual (/command )
Language Any executable N/A N/A
Use Case Automation, validation Reference, patterns Quick tasks
Security Requires confirmation No special permissions Inherits from session
Examples:
-
Hook: Auto-format files on save
-
Skill: Reference guide for testing patterns
-
Command: /review-pr quick code review
Related Resources
-
claude-hook-writer skill - Detailed hook development guidance
-
typescript-hook-writer skill - TypeScript-specific hook development
-
Claude Code Docs
-
Schema
Checklist for New Hooks
Before publishing:
-
Shebang line included
-
File is executable (chmod +x )
-
Validates all stdin input
-
Quotes all variables
-
Handles missing dependencies gracefully
-
Uses appropriate exit codes
-
Logs errors to stderr or file
-
Tests with edge cases (spaces, Unicode, missing fields)
-
Documents dependencies in HOOK.md
-
Includes installation instructions
-
Source code included in documentation
-
Clear description and tags in prpm.json
-
Version number is semantic