creating-claude-hooks by pr-pm
Use when creating or publishing Claude Code hooks - covers executable format, event types, JSON I/O, exit codes, security requirements, and PRPM package structure
Content & Writing
80 Stars
11 Forks
Updated Jan 16, 2026, 03:39 PM
Why Use This
This skill provides specialized capabilities for pr-pm's codebase.
Use Cases
- Developing new features in the pr-pm repository
- Refactoring existing code to follow pr-pm standards
- Understanding and working with pr-pm's codebase structure
Install Guide
2 steps- 1
Skip this step if Ananke is already installed.
- 2
Skill Snapshot
Auto scan of skill assets. Informational only.
Valid SKILL.md
Checks against SKILL.md specification
Source & Community
Skill Stats
SKILL.md 686 Lines
Total Files 1
Total Size 0 B
License NOASSERTION
---
name: creating-claude-hooks
description: Use when creating or publishing Claude Code hooks - covers executable format, event types, JSON I/O, exit codes, security requirements, and PRPM package structure
---
# 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's `hooks.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):
```json
{
"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`:
```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
```json
{
"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
```bash
#!/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
```typescript
#!/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
```json
{
"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
```json
{
"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:
1. **Have a shebang line:**
```bash
#!/bin/bash
#!/usr/bin/env node
#!/usr/bin/env python3
```
2. **Be executable:**
```bash
chmod +x .claude/hooks/PreToolUse
```
3. **Handle JSON input from stdin**
4. **Exit with appropriate code**
## JSON Input Structure
Hooks receive JSON via stdin:
```json
{
"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
```json
{
"hooks": {
"PostToolUse": [{
"matcher": "Write|Edit",
"hooks": [{
"type": "command",
"command": "${CLAUDE_PLUGIN_ROOT}/scripts/format.sh",
"timeout": 5000
}]
}]
}
}
```
### Pattern 2: Block Sensitive Files
```json
{
"hooks": {
"PreToolUse": [{
"matcher": "Write|Edit|Read",
"hooks": [{
"type": "command",
"command": "${CLAUDE_PLUGIN_ROOT}/scripts/block-sensitive.sh"
}]
}]
}
}
```
### Pattern 3: Test Verification Before Stop
```json
{
"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
```json
{
"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):
```json
{
"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
```bash
# 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:
```bash
#!/bin/bash
#!/usr/bin/env node
#!/usr/bin/env python3
```
### 4. Secure Sensitive Files
```bash
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
```bash
# WRONG - breaks on spaces
prettier --write $FILE
# RIGHT - handles spaces
prettier --write "$FILE"
```
### 6. Log for Debugging
```bash
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
```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
```markdown
---
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
```bash
# 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
```bash
# 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
```bash
# 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](https://docs.claude.com/claude-code)
- [Schema](https://github.com/pr-pm/prpm/blob/main/packages/converters/schemas/claude-hook.schema.json)
## 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
Name Size