creating-claude-hooks
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
Two Hook Configuration Methods
Method 1: JSON Configuration (Recommended)
- Configure in
.claude/settings.json,~/.claude/settings.json, or plugin'shooks.json - Supports both command hooks and prompt hooks
- More flexible, supports matchers and timeouts
Method 2: Executable Files (Legacy)
- Place executables in
.claude/hooks/<event-name> - Simpler but less configurable
Hook Types
| Type | Description | Speed | Use Case |
|---|---|---|---|
| Command | Runs external script | Fast (ms) | Formatting, logging, file checks |
| Prompt | Uses LLM reasoning | Slow (2-10s) | Complex validation, security analysis |
Available Events
| Event | When It Fires | Can Block? | Common Use Cases |
|---|---|---|---|
PreToolUse |
Before tool execution | Yes (exit 2) | Validation, permission checks, input modification |
PostToolUse |
After tool completes | No | Formatting, logging, cleanup |
UserPromptSubmit |
Before user input processes | Yes | Prompt validation, enhancement |
SessionStart |
New session begins | No | Environment setup, context loading |
Stop |
When assistant finishes | No | Cleanup, summary, verification |
SubagentStop |
When subagent finishes | No | Subagent result processing |
PreCompact |
Before context compaction | No | Save important context |
Notification |
During alerts | No | Desktop notifications, logging |
PermissionRequest |
When permission needed | Yes | Custom permission handling |
Exit Codes
| Code | Meaning | Behavior |
|---|---|---|
0 |
Success | Continue normally |
2 |
Block | Stop operation (PreToolUse only) |
1 or other |
Error | Log error, continue |
JSON Hook Configuration
Settings-Based Hooks
Configure hooks in .claude/settings.json (project) or ~/.claude/settings.json (global):
{
"hooks": {
"PreToolUse": [{
"matcher": "Write|Edit",
"hooks": [{
"type": "command",
"command": "/path/to/validate-write.sh",
"timeout": 5000
}]
}],
"PostToolUse": [{
"matcher": "Write|Edit",
"hooks": [{
"type": "command",
"command": "/path/to/format-file.sh"
}]
}],
"Stop": [{
"matcher": "*",
"hooks": [{
"type": "prompt",
"prompt": "Verify all requested changes were completed."
}]
}]
}
}
Plugin hooks.json
For PRPM packages, use hook.json:
{
"hooks": {
"PreToolUse": [{
"matcher": "Write",
"hooks": [{
"type": "command",
"command": "${CLAUDE_PLUGIN_ROOT}/scripts/validate.sh",
"timeout": 5000
}]
}]
}
}
Matcher Patterns
| Pattern | Matches |
|---|---|
"Write" |
Only Write tool |
"Write|Edit" |
Write OR Edit tools |
"Bash" |
Only Bash tool |
"mcp__github__*" |
All GitHub MCP tools |
"*" |
All tools (use sparingly) |
Hook Options
{
"type": "command",
"command": "./my-hook.sh",
"timeout": 5000,
"once": true,
"continue": true,
"stopReason": "Message when blocked",
"suppressOutput": false,
"systemMessage": "Warning to show user"
}
| Option | Type | Default | Description |
|---|---|---|---|
timeout |
number | 60000 | Max execution time in ms |
once |
boolean | false | Run only once per session |
continue |
boolean | true | Continue after hook completes |
stopReason |
string | - | Message when continue=false |
suppressOutput |
boolean | false | Hide stdout from transcript |
systemMessage |
string | - | Warning message to user |
Command Hooks
Command hooks run external scripts. They're fast and deterministic.
Shell Script Hook
#!/bin/bash
set -euo pipefail
# Read JSON input
INPUT=$(cat)
FILE=$(echo "$INPUT" | jq -r '.input.file_path // empty')
# Validate
[[ -n "$FILE" ]] || exit 0
[[ -f "$FILE" ]] || exit 0
# Block sensitive files
case "$FILE" in
*.env|*.pem|*.key)
echo "Blocked: $FILE is sensitive" >&2
exit 2
;;
esac
exit 0
TypeScript Hook
#!/usr/bin/env node
import { readFileSync } from 'fs';
const input = JSON.parse(readFileSync(0, 'utf-8'));
const filePath = input.input?.file_path;
if (!filePath) process.exit(0);
// Block .env files
if (filePath.endsWith('.env')) {
console.error('Blocked: Cannot modify .env files');
process.exit(2);
}
process.exit(0);
Prompt Hooks
Prompt hooks use LLM reasoning for complex validation. Use sparingly - they take 2-10 seconds.
Basic Prompt Hook
{
"hooks": {
"PreToolUse": [{
"matcher": "Write",
"hooks": [{
"type": "prompt",
"prompt": "Check if the content being written contains hardcoded secrets, API keys, or credentials. If found, block the operation."
}]
}]
}
}
Prompt Hook with Schema Validation
{
"hooks": {
"PreToolUse": [{
"matcher": "Write|Edit",
"hooks": [{
"type": "prompt",
"prompt": "Analyze the file content for security issues. Return your decision.",
"schema": {
"type": "object",
"properties": {
"decision": {
"type": "string",
"enum": ["allow", "block"]
},
"reason": {
"type": "string"
},
"severity": {
"type": "string",
"enum": ["low", "medium", "high", "critical"]
}
},
"required": ["decision"]
}
}]
}]
}
}
When to Use Prompt Hooks
Good use cases:
- Security analysis that requires understanding code context
- Detecting logic errors or anti-patterns
- Validating architectural decisions
- Complex permission checks
Avoid for:
- Simple pattern matching (use command hooks)
- File extension checks
- Path validation
- Anything that can be done with regex
File-Based Hooks (Legacy)
Simpler approach - place executables directly in hooks directory.
File Location
Project hooks:
.claude/hooks/PreToolUse
.claude/hooks/PostToolUse
.claude/hooks/SessionStart
User-global hooks:
~/.claude/hooks/PreToolUse
~/.claude/hooks/Stop
Requirements
Every file-based hook MUST:
- Have a shebang line:
#!/bin/bash
#!/usr/bin/env node
#!/usr/bin/env python3
- Be executable:
chmod +x .claude/hooks/PreToolUse
-
Handle JSON input from stdin
-
Exit with appropriate code
JSON Input Structure
Hooks receive JSON via stdin:
{
"session_id": "abc123",
"transcript_path": "/path/to/transcript.jsonl",
"current_dir": "/path/to/project",
"tool_name": "Write",
"input": {
"file_path": "/path/to/file.ts",
"content": "file contents...",
"command": "npm test",
"old_string": "...",
"new_string": "..."
}
}
Tool-Specific Input Fields
| Tool | Available Fields |
|---|---|
| Write | file_path, content |
| Edit | file_path, old_string, new_string |
| Read | file_path |
| Bash | command |
| Glob | pattern, path |
| Grep | pattern, path |
Environment Variables
Available in hook execution:
| Variable | Description |
|---|---|
CLAUDE_PROJECT_DIR |
Project root directory |
CLAUDE_CURRENT_DIR |
Current working directory |
CLAUDE_PLUGIN_ROOT |
Hook installation directory |
CLAUDE_ENV_FILE |
File for persisting variables |
SESSION_ID |
Current session identifier |
Common Patterns
Pattern 1: Format on Save
{
"hooks": {
"PostToolUse": [{
"matcher": "Write|Edit",
"hooks": [{
"type": "command",
"command": "${CLAUDE_PLUGIN_ROOT}/scripts/format.sh",
"timeout": 5000
}]
}]
}
}
Pattern 2: Block Sensitive Files
{
"hooks": {
"PreToolUse": [{
"matcher": "Write|Edit|Read",
"hooks": [{
"type": "command",
"command": "${CLAUDE_PLUGIN_ROOT}/scripts/block-sensitive.sh"
}]
}]
}
}
Pattern 3: Test Verification Before Stop
{
"hooks": {
"Stop": [{
"matcher": "*",
"hooks": [{
"type": "prompt",
"prompt": "Before finishing, verify: 1) All tests pass 2) No linting errors 3) Types check. If any issues, list them."
}]
}]
}
}
Pattern 4: Session Context Loading
{
"hooks": {
"SessionStart": [{
"matcher": "*",
"hooks": [{
"type": "command",
"command": "${CLAUDE_PLUGIN_ROOT}/scripts/load-context.sh",
"once": true
}]
}]
}
}
Pattern 5: Multi-Stage Validation
Combine PreToolUse (validate) with PostToolUse (verify):
{
"hooks": {
"PreToolUse": [{
"matcher": "Write",
"hooks": [{
"type": "command",
"command": "./validate-before.sh"
}]
}],
"PostToolUse": [{
"matcher": "Write",
"hooks": [{
"type": "command",
"command": "./verify-after.sh"
}]
}]
}
}
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} |
Using * matcher |
Runs on everything | Be specific: Write|Edit |
| Prompt hooks everywhere | Slow experience | Use only for complex logic |
Best Practices
1. Keep Hooks Fast
Target < 100ms for PreToolUse hooks:
- Cache results where possible
- Run heavy operations in background
- Use specific matchers, not wildcards
2. 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
3. Use Shebangs
Always start with shebang:
#!/bin/bash
#!/usr/bin/env node
#!/usr/bin/env python3
4. 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
5. Quote All Variables
# WRONG - breaks on spaces
prettier --write $FILE
# RIGHT - handles spaces
prettier --write "$FILE"
6. 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-prquick 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
More from pr-pm/prpm
human-writing
Write content that sounds natural, conversational, and authentically human - avoiding AI-generated patterns, corporate speak, and generic phrasing
168creating-opencode-agents
Use when creating OpenCode agents - provides markdown format with YAML frontmatter, mode/tools/permission configuration, and best practices for specialized AI assistants
25creating-agents-md
Use when creating agents.md files - provides plain markdown format with NO frontmatter, free-form structure, and project context guidelines for AI coding assistants
20self-improving
Use when starting infrastructure, testing, deployment, or framework-specific tasks - automatically searches PRPM registry for relevant expertise packages and suggests installation to enhance capabilities for the current task
19creating-opencode-plugins
Use when creating OpenCode plugins that hook into command, file, LSP, message, permission, server, session, todo, tool, or TUI events - provides plugin structure, event API specifications, and implementation patterns for JavaScript/TypeScript event-driven modules
18typescript-type-safety
Use when encountering TypeScript any types, type errors, or lax type checking - eliminates type holes and enforces strict type safety through proper interfaces, type guards, and module augmentation
16