
Claude Hook Writer
- 314 installs
- 202 repo stars
- Updated August 4, 2026
- secondsky/claude-skills
claude-hook-writer is a Claude Code agent skill that guides developers to write secure, reliable, and performant hooks with design checklists, reference templates, and debugging patterns for PreToolUse and PostToolUse ev
About
claude-hook-writer is version 2.0.0 in secondsky/claude-skills, optimized in December 2025 with about 55% token savings versus prior guidance. The skill validates hook design choices—event targets, matchers, hook type command versus prompt, exit codes—and bundles six on-demand reference files covering security requirements, reliability and performance, code templates, testing and debugging, publishing, and quick syntax lookup. It prevents five documented common hook pitfalls and supports creating, reviewing, debugging, optimizing, and publishing hooks including PRPM packages. Developers invoke claude-hook-writer when building PreToolUse or PostToolUse automation that runs with user permissions and must sanitize inputs, quote shell variables, enforce timeouts, and avoid blocking the agent loop on slow work.
- claude-hook-writer
Claude Hook Writer by the numbers
- 314 all-time installs (skills.sh)
- +12 installs in the week ending Aug 5, 2026 (Skillselion tracking)
- Ranked #1,300 of 4,347 Backend & APIs skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/secondsky/claude-skills --skill claude-hook-writerAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 314 |
|---|---|
| repo stars | ★ 202 |
| Last updated | August 4, 2026 |
| Repository | secondsky/claude-skills ↗ |
How do you write secure Claude Code hooks?
Use claude-hook-writer for development tasks
Who is it for?
Developers creating or auditing Claude Code hooks that enforce formatting, block sensitive operations, or inject context on agent tool events.
Skip if: Skip claude-hook-writer when you need generic CI scripts unrelated to Claude Code hook events or MCP server authoring without hook lifecycle concerns.
When should I use this skill?
User is designing, reviewing, debugging, optimizing, or publishing Claude Code hooks or mentions PreToolUse, PostToolUse, or hook failures.
What you get
Reviewed hook design, security-hardened hook scripts, reference-backed templates, and debugging guidance for Claude Code PreToolUse and PostToolUse handlers.
- Hook design checklist
- Secure hook script templates
- Debugging and test guidance
By the numbers
- Skill version 2.0.0 with six bundled reference files
- Metadata cites prevention of five common hook pitfalls
- Optimization notes report about 55% token savings
Files
Claude Hook Writer
Status: Production Ready Version: 2.0.0 (Optimized with progressive disclosure) Last Updated: 2025-12-17
---
Overview
Expert guidance for writing secure, reliable, and performant Claude Code hooks. This skill validates design decisions, enforces best practices, and prevents common pitfalls.
---
When to Use This Skill
- Designing a new Claude Code hook
- Reviewing existing hook code
- Debugging hook failures
- Optimizing slow hooks
- Securing hooks that handle sensitive data
- Publishing hooks as PRPM packages
---
Core Principles
1. Security is Non-Negotiable
Hooks execute automatically with user permissions and can read, modify, or delete any file the user can access.
ALWAYS validate and sanitize all input. Hooks receive JSON via stdin—never trust it blindly.
For complete security patterns: Load references/security-requirements.md when implementing validation or securing hooks.
2. Reliability Over Features
A hook that works 99% of the time is a broken hook. Edge cases (Unicode filenames, spaces in paths, missing tools) will happen.
Test with edge cases before deploying.
For reliability patterns: Load references/reliability-performance.md when handling errors or edge cases.
3. Performance Matters
Hooks block operations. A 5-second hook means Claude waits 5 seconds before continuing.
Keep hooks fast. Run heavy operations in background.
For performance optimization: Load references/reliability-performance.md when optimizing hook speed.
4. Fail Gracefully
Missing dependencies, malformed input, and disk errors will occur.
Handle errors explicitly. Log failures. Return meaningful exit codes.
---
Hook Design Checklist
Before writing code, answer these questions:
What Event Does This Hook Target?
PreToolUse- Before tool execution (modify input, validate, block)PostToolUse- After tool completes (format, log, cleanup)UserPromptSubmit- Before user input processes (validate, enhance)SessionStart- When Claude Code starts (setup, env check)SessionEnd- When Claude Code exits (cleanup, persist state)Notification- During alerts (desktop notifications, logging)Stop/SubagentStop- When responses finish (cleanup, summary)PreCompact- Before context compaction (save important context)
Common mistake: Using PostToolUse for validation (too late—tool already ran). Use PreToolUse to block operations.
Which Tools Should Trigger This Hook?
Be specific. matcher: "*" runs on every tool call.
Good matchers:
"Write"- Only file writes"Edit|Write"- File modifications"Bash"- Shell commands"mcp__github__*"- All GitHub MCP tools
Bad matchers:
"*"- Everything (use only for logging/metrics)
What Input Does This Hook Need?
Different tools provide different input. Check what's available:
# PreToolUse / PostToolUse
{
"input": {
"file_path": "/path/to/file.ts", // Read, Write, Edit
"command": "npm test", // Bash
"old_string": "...", // Edit
"new_string": "..." // Edit
}
}Validate fields exist before using them:
FILE=$(echo "$INPUT" | jq -r '.input.file_path // empty')
if [[ -z "$FILE" ]]; then
echo "No file path provided" >&2
exit 1
fiShould This Be a Command Hook or Prompt Hook?
Command hooks (type: "command"):
- Fast (milliseconds)
- Deterministic
- Good for: formatting, logging, file checks
Prompt hooks (type: "prompt"):
- Slow (2-10 seconds)
- Context-aware (uses LLM)
- Good for: complex validation, security analysis, intent detection
Rule of thumb: Use command hooks unless you need LLM reasoning.
What Exit Code Communicates Success/Failure?
exit 0- Success (continue operation)exit 2- Block operation (show error to Claude)exit 1or other - Non-blocking error (log but continue)
For PreToolUse hooks:
- Exit 2 blocks the tool from running
- Exit 0 allows it (optionally with modified input)
For PostToolUse hooks:
- Exit codes don't block (tool already ran)
- Use exit 0 for success, 1 for logging errors
---
Top 5 Pitfalls (Must Know)
Pitfall #1: Not Quoting Variables
Error: Hooks break on filenames with spaces or special characters
Why: Unquoted variables split on whitespace
Example:
# ❌ WRONG - breaks on "my file.txt"
cat $FILE
prettier --write $FILE
rm $FILE
# ✅ RIGHT - handles spaces and special chars
cat "$FILE"
prettier --write "$FILE"
rm "$FILE"Why this matters: Files with spaces ("my file.txt"), Unicode ("文件.txt"), or special chars ("file (1).txt") are common.
For quoting best practices: Load references/security-requirements.md for comprehensive input handling patterns.
---
Pitfall #2: Trusting Input Without Validation
Error: Hook executes on malicious or malformed input
Why: Not validating JSON fields before using them
Example:
# ❌ DANGEROUS - no validation
FILE=$(jq -r '.input.file_path')
rm "$FILE" # Could delete ../../../etc/passwd
# ✅ SAFE - validate first
FILE=$(jq -r '.input.file_path // empty')
[[ -n "$FILE" ]] || exit 1
[[ "$FILE" == "$CLAUDE_PROJECT_DIR"* ]] || exit 2
[[ "$FILE" != *".."* ]] || exit 2
rm "$FILE"Why this matters: Prevents path traversal attacks, protects files outside project, prevents malformed input crashes.
For complete security patterns: Load references/security-requirements.md.
---
Pitfall #3: Blocking Operations Too Long
Error: Hook takes 30+ seconds, blocking Claude
Why: Running expensive operations (tests, builds) synchronously in hook
Example:
# ❌ BLOCKS Claude for 30 seconds
npm test
npm run build
# ✅ RUN IN BACKGROUND - returns immediately
(npm test > /tmp/test-results.log 2>&1 &)
(npm run build > /tmp/build.log 2>&1 &)
exit 0Why this matters: Slow hooks create bad user experience. Target < 100ms for PreToolUse, < 500ms for PostToolUse.
For performance optimization: Load references/reliability-performance.md.
---
Pitfall #4: Wrong Exit Code for Blocking
Error: PreToolUse hook doesn't actually block the operation
Why: Using exit 1 instead of exit 2
Example:
# ❌ WRONG - logs error but doesn't block
if [[ $FILE == ".env" ]]; then
echo "Don't edit .env" >&2
exit 1 # Tool still runs!
fi
# ✅ RIGHT - actually blocks
if [[ $FILE == ".env" ]]; then
echo "Blocked: .env is protected" >&2
exit 2 # Tool is blocked
fiWhy this matters: Exit 1 only logs errors. Exit 2 is required to block in PreToolUse hooks.
For exit code patterns: Load references/hook-templates.md for complete hook response patterns.
---
Pitfall #5: Assuming Tools Exist
Error: Hook crashes when dependency is missing
Why: Not checking if tool is installed before using
Example:
# ❌ BREAKS if prettier not installed
prettier --write "$FILE"
# ✅ SAFE - check first
if command -v prettier &>/dev/null; then
prettier --write "$FILE"
else
echo "prettier not installed, skipping" >&2
exit 0 # Success exit, just skip
fiWhy this matters: Users may not have all tools installed. Hooks should degrade gracefully.
For reliability patterns: Load references/reliability-performance.md.
---
Critical Rules
Always Do
✅ Validate all JSON input before using (jq -r '... // empty') ✅ Quote all variables containing paths or user input ✅ Use absolute paths for scripts (${CLAUDE_PLUGIN_ROOT}/...) ✅ Block sensitive files (.env, *.key, credentials) ✅ Check if required tools exist (command -v toolname) ✅ Set reasonable timeouts (< 5s for PreToolUse) ✅ Run heavy operations in background ✅ Test with edge cases (spaces, Unicode, special chars) ✅ Use exit 2 to block in PreToolUse hooks ✅ Log errors to stderr or file, not stdout
Never Do
❌ Trust JSON input without validation ❌ Use unquoted variables ($FILE instead of "$FILE") ❌ Use relative paths for scripts ❌ Skip path sanitization (check for .., validate in project) ❌ Assume tools are installed ❌ Block for > 1 second in PreToolUse hooks ❌ Use exit 1 when you mean to block (use exit 2) ❌ Log sensitive data to stdout or files ❌ Use matcher: "*" unless truly necessary
---
When to Load References
Load reference files when working on specific hook aspects:
Security Requirements (references/security-requirements.md)
Load when:
- Implementing input validation and sanitization
- Securing hooks that handle sensitive data
- Blocking sensitive files (
.env, keys, credentials) - Preventing path traversal attacks
- Understanding security vulnerabilities and best practices
- Testing security with malicious input
Reliability & Performance (references/reliability-performance.md)
Load when:
- Handling missing dependencies or tools
- Setting timeouts and handling slow operations
- Optimizing hook performance (< 100ms target)
- Running heavy operations in background
- Caching expensive results
- Testing with edge cases (Unicode, spaces, deep paths)
- Deduplicating expensive operations
Code Templates (references/code-templates.md)
Load when:
- Starting a new hook and need working examples
- Implementing format-on-save functionality
- Blocking sensitive files from modification
- Logging commands or operations
- Using prompt-based security analysis
- Customizing templates for specific use cases
Testing & Debugging (references/testing-debugging.md)
Load when:
- Writing test cases for hooks
- Debugging hook failures or unexpected behavior
- Testing with edge cases (malformed JSON, missing fields)
- Checking hook execution in transcript (Ctrl-R)
- Profiling hook performance
- Creating automated test suites
Publishing Guide (references/publishing-guide.md)
Load when:
- Publishing hooks to PRPM registry
- Creating package manifest (prpm.json)
- Configuring hook.json with advanced options
- Using
continue,stopReason,suppressOutput,systemMessage - Writing README.md for users
- Understanding versioning and publishing commands
Quick Reference (references/quick-reference.md)
Load when:
- Need quick syntax lookup (exit codes, jq patterns)
- Looking up environment variables
- Finding common bash patterns (file validation, background execution)
- Checking hook events and matchers
- Need performance tips summary
- Looking up JSON input structure
---
Final Checklist
Before publishing a hook:
- [ ] Validates all stdin input with jq
- [ ] Quotes all variables
- [ ] Uses absolute paths for scripts
- [ ] Blocks sensitive files (
.env,*.key, etc.) - [ ] Handles missing tools gracefully
- [ ] Sets reasonable timeout (< 5s for PreToolUse)
- [ ] Logs errors to stderr or file, not stdout
- [ ] Tests with edge cases (spaces, Unicode, malformed JSON)
- [ ] Tests in real Claude Code session
- [ ] Documents dependencies in README
- [ ] Uses semantic versioning
- [ ] Clear description and tags
---
Using Bundled Resources
This skill includes 6 reference files for on-demand loading:
Security & Reliability (2 files):
security-requirements.md- Input validation, path sanitization, blocking sensitive filesreliability-performance.md- Error handling, timeouts, performance optimization
Implementation (2 files):
code-templates.md- Working hook examples (format-on-save, block-sensitive, logger, etc.)quick-reference.md- Fast syntax lookup (exit codes, jq patterns, environment vars)
Testing & Publishing (2 files):
testing-debugging.md- Test patterns, edge cases, debugging techniquespublishing-guide.md- PRPM packaging, advanced configuration, README template
Load references on-demand when specific knowledge is needed. See "When to Load References" section for triggers.
---
Resources
---
Last verified: 2025-12-17 | Version: 2.0.0
Code Templates
Complete working hook examples for common use cases.
---
Template 1: Format On Save Hook
Auto-format files after editing or writing.
Bash Script (format-on-save.sh):
#!/bin/bash
set -euo pipefail
# Parse input
INPUT=$(cat)
FILE=$(echo "$INPUT" | jq -r '.input.file_path // empty')
# Validate
[[ -n "$FILE" ]] || exit 0
[[ -f "$FILE" ]] || exit 0
[[ "$FILE" == "$CLAUDE_PROJECT_DIR"* ]] || exit 0
# Check formatter installed
if ! command -v prettier &> /dev/null; then
exit 0
fi
# Format by extension
case "$FILE" in
*.ts|*.tsx|*.js|*.jsx)
prettier --write "$FILE" 2>/dev/null || exit 0
;;
*.py)
black "$FILE" 2>/dev/null || exit 0
;;
*.go)
gofmt -w "$FILE" 2>/dev/null || exit 0
;;
esacJSON Config:
{
"hooks": {
"PostToolUse": [{
"matcher": "Edit|Write",
"hooks": [{
"type": "command",
"command": "${CLAUDE_PLUGIN_ROOT}/scripts/format-on-save.sh",
"timeout": 5000
}]
}]
}
}Use when: You want automatic formatting after file changes.
---
Template 2: Block Sensitive Files Hook
Prevent modification of sensitive files (.env, keys, credentials).
Bash Script (block-sensitive.sh):
#!/bin/bash
set -euo pipefail
INPUT=$(cat)
FILE=$(echo "$INPUT" | jq -r '.input.file_path // empty')
[[ -n "$FILE" ]] || exit 0
# Sensitive patterns
BLOCKED=(
".env"
".env.*"
"*.pem"
"*.key"
"*secret*"
"*credential*"
".git/*"
)
for pattern in "${BLOCKED[@]}"; do
# Use case for glob matching
case "$FILE" in
$pattern)
echo "Blocked: $FILE is a sensitive file" >&2
echo " Pattern: $pattern" >&2
exit 2 # Block operation
;;
esac
done
exit 0 # AllowJSON Config:
{
"hooks": {
"PreToolUse": [{
"matcher": "Edit|Write",
"hooks": [{
"type": "command",
"command": "${CLAUDE_PLUGIN_ROOT}/scripts/block-sensitive.sh"
}]
}]
}
}Use when: You want to protect sensitive files from accidental modification.
---
Template 3: Command Logger Hook
Log all bash commands executed by Claude.
Bash Script (command-logger.sh):
#!/bin/bash
set -euo pipefail
INPUT=$(cat)
COMMAND=$(echo "$INPUT" | jq -r '.input.command // empty')
[[ -n "$COMMAND" ]] || exit 0
LOG_FILE=~/.claude-hooks/commands.log
mkdir -p "$(dirname "$LOG_FILE")"
# Log with timestamp and context
{
echo "---"
echo "Time: $(date '+%Y-%m-%d %H:%M:%S')"
echo "Directory: $CLAUDE_CURRENT_DIR"
echo "Command: $COMMAND"
} >> "$LOG_FILE"
exit 0JSON Config:
{
"hooks": {
"PreToolUse": [{
"matcher": "Bash",
"hooks": [{
"type": "command",
"command": "${CLAUDE_PLUGIN_ROOT}/scripts/command-logger.sh"
}]
}]
}
}Use when: You want audit trail of all commands Claude executes.
---
Template 4: Prompt-Based Security Hook
Use LLM to analyze file content for secrets before writing.
JSON Config (no bash script needed):
{
"hooks": {
"PreToolUse": [{
"matcher": "Write",
"hooks": [{
"type": "prompt",
"prompt": "Analyze the file content being written to ${input.file_path}. Check if it contains: hardcoded API keys, AWS credentials, private keys, passwords, or secrets. Return {\"decision\": \"block\", \"reason\": \"<specific issue>\"} if found, otherwise {\"decision\": \"allow\"}.",
"schema": {
"type": "object",
"properties": {
"decision": {"enum": ["allow", "block"]},
"reason": {"type": "string"}
},
"required": ["decision"]
}
}]
}]
}
}Use when: You need AI-powered security analysis (slower, 2-10 seconds per file).
Trade-offs:
- ✅ Pro: Detects complex secret patterns
- ❌ Con: Slow (blocks for 2-10 seconds)
- ❌ Con: Uses AI API tokens
---
Template 5: Lint On Save Hook
Run linter after file modifications.
Bash Script (lint-on-save.sh):
#!/bin/bash
set -euo pipefail
INPUT=$(cat)
FILE=$(echo "$INPUT" | jq -r '.input.file_path // empty')
# Validate
[[ -n "$FILE" ]] || exit 0
[[ -f "$FILE" ]] || exit 0
[[ "$FILE" == "$CLAUDE_PROJECT_DIR"* ]] || exit 0
# Only lint TypeScript/JavaScript
[[ "$FILE" == *.ts ]] || [[ "$FILE" == *.tsx ]] || [[ "$FILE" == *.js ]] || [[ "$FILE" == *.jsx ]] || exit 0
# Check eslint installed
if ! command -v eslint &> /dev/null; then
exit 0
fi
# Run linter (suppress output, just check exit code)
if ! eslint "$FILE" &> /dev/null; then
echo "⚠️ Lint errors in $FILE - run 'eslint $FILE' to see details" >&2
fi
exit 0 # Don't block, just warnJSON Config:
{
"hooks": {
"PostToolUse": [{
"matcher": "Edit|Write",
"hooks": [{
"type": "command",
"command": "${CLAUDE_PLUGIN_ROOT}/scripts/lint-on-save.sh",
"timeout": 3000
}]
}]
}
}Use when: You want lint warnings after editing files.
---
Template 6: Git Auto-Commit Hook
Automatically commit changes after file modifications.
Bash Script (auto-commit.sh):
#!/bin/bash
set -euo pipefail
INPUT=$(cat)
FILE=$(echo "$INPUT" | jq -r '.input.file_path // empty')
[[ -n "$FILE" ]] || exit 0
[[ -f "$FILE" ]] || exit 0
# Run in background to not block Claude
(
cd "$CLAUDE_PROJECT_DIR" || exit
# Add file
git add "$FILE" 2>/dev/null || exit
# Commit with auto-generated message
FILENAME=$(basename "$FILE")
git commit -m "Auto-commit: Update $FILENAME" 2>/dev/null || exit
echo "[$(date)] Auto-committed: $FILE" >> ~/.claude-hooks/commits.log
) &
exit 0 # Return immediatelyJSON Config:
{
"hooks": {
"PostToolUse": [{
"matcher": "Edit|Write",
"hooks": [{
"type": "command",
"command": "${CLAUDE_PLUGIN_ROOT}/scripts/auto-commit.sh"
}]
}]
}
}Use when: You want automatic git commits (use carefully!).
Warning: Creates many commits. Consider using interactive staging instead.
---
Template Customization
All templates can be customized:
Add File Extension Filters
# Only process specific extensions
case "$FILE" in
*.ts|*.tsx)
# Your logic here
;;
*)
exit 0 # Skip other files
;;
esacAdd Directory Filters
# Only process files in specific directories
if [[ "$FILE" == "$CLAUDE_PROJECT_DIR/src/"* ]]; then
# Your logic here
else
exit 0 # Skip files outside src/
fiAdd Custom Blocklists
# Extend blocked patterns
BLOCKED+=(
"node_modules/*"
"dist/*"
"build/*"
"*.min.js"
)Add Logging
LOG_FILE=~/.claude-hooks/my-hook.log
echo "[$(date)] Processed: $FILE" >> "$LOG_FILE"---
Best Practices for Templates
1. Always validate input: Check JSON fields exist before using 2. Always check file exists: Don't assume files exist 3. Always use absolute paths: For scripts and referenced files 4. Always quote variables: Handle spaces in filenames 5. Always handle missing tools: Check with command -v 6. Always set timeouts: Prevent hanging hooks 7. Always use `set -euo pipefail`: Exit on errors 8. Run heavy operations in background: Don't block Claude
---
Testing Templates
Test each template with edge cases:
# Test with normal file
echo '{"input":{"file_path":"test.ts"}}' | ./template.sh
echo "Exit code: $?"
# Test with spaces in filename
echo '{"input":{"file_path":"my file.ts"}}' | ./template.sh
# Test with missing file
echo '{"input":{"file_path":"nonexistent.ts"}}' | ./template.sh
# Test with sensitive file
echo '{"input":{"file_path":".env"}}' | ./template.sh
echo "Exit code: $?" # Should be 2 for block-sensitive template
# Test with malformed JSON
echo 'not json' | ./template.sh
echo "Exit code: $?" # Should be 1Publishing Hooks as PRPM Packages
Guide for packaging and publishing hooks to the PRPM registry.
---
Package Structure
my-hook/
├── prpm.json # Package manifest
├── hook.json # Hook configuration
├── scripts/
│ └── my-hook.sh # Hook script
└── README.md # Documentation---
prpm.json
Package manifest with metadata:
{
"name": "@yourname/my-hook",
"version": "1.0.0",
"description": "Brief description shown in search results",
"author": "Your Name",
"format": "claude",
"subtype": "hook",
"tags": [
"formatting",
"security",
"automation"
],
"main": "hook.json",
"scripts": {
"test": "./test-hook.sh"
}
}---
hook.json
Hook configuration with ${CLAUDE_PLUGIN_ROOT} for portability:
{
"hooks": {
"PostToolUse": [{
"matcher": "Edit|Write",
"hooks": [{
"type": "command",
"command": "${CLAUDE_PLUGIN_ROOT}/scripts/my-hook.sh",
"timeout": 5000
}]
}]
}
}Always use `${CLAUDE_PLUGIN_ROOT}` to reference scripts.
---
Advanced Hook Configuration
All hook types support optional fields:
{
"hooks": {
"PreToolUse": [{
"matcher": "Write",
"hooks": [{
"type": "command",
"command": "./my-hook.sh",
"timeout": 5000,
"continue": true,
"stopReason": "string",
"suppressOutput": false,
"systemMessage": "string"
}]
}]
}
}continue (boolean, default: true)
Controls whether Claude continues after hook execution.
When to use `false`:
- Security hooks that must block operations
- Validation hooks that found critical errors
{
"continue": false,
"stopReason": "Security validation failed. Review detected issues."
}stopReason (string)
Message displayed when continue: false. Explain why and what action is needed.
suppressOutput (boolean, default: false)
Hides hook stdout from transcript. Stderr always shown.
When to use `true`:
- Verbose output not useful to users
- Background operations
systemMessage (string)
Warning shown to user (non-blocking).
Difference from `stopReason`:
systemMessage: Informational, continuesstopReason: Critical, requirescontinue: false
---
README.md Template
# My Hook
Brief description of what this hook does.
## What It Does
- Clear bullet points
- Which events it triggers on
- Which tools it matches
## Installation
\`\`\`bash
prpm install @yourname/my-hook
\`\`\`
## Requirements
- prettier (install: `npm install -g prettier`)
- jq (install: `brew install jq`)
## Configuration
Optional: How to customize behavior.
## Examples
Show example output or behavior.
## Troubleshooting
Common issues and fixes.---
Publishing Commands
# Test locally first
prpm test
# Publish
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---
Pre-Publish Checklist
- [ ] Validates all stdin input
- [ ] Quotes all variables
- [ ] Uses absolute paths for scripts
- [ ] Blocks sensitive files
- [ ] Handles missing tools gracefully
- [ ] Sets reasonable timeout
- [ ] Logs errors to stderr or file
- [ ] Tests with edge cases
- [ ] Tests in real Claude session
- [ ] Documents dependencies
- [ ] README includes examples
- [ ] Semantic version number
- [ ] Clear description and tags
---
Best Practices
1. Clear description: Users find your hook via search 2. Comprehensive README: Include examples and troubleshooting 3. Semantic versioning: Breaking changes = major version 4. Test before publishing: Run prpm test 5. Document dependencies: List all required tools 6. Provide examples: Show expected behavior 7. Handle errors gracefully: Don't crash on edge cases
Quick Reference
Fast lookup for hook syntax, exit codes, environment variables, and common patterns.
---
Exit Codes
0= Success (continue operation)2= Block operation (PreToolUse only)1or other = Non-blocking error (logged but continues)
For PreToolUse hooks:
- Exit 2 blocks the tool from running
- Exit 0 allows it (optionally with modified input)
For PostToolUse hooks:
- Exit codes don't block (tool already ran)
- Use exit 0 for success, 1 for logging errors
---
Hook Configuration Fields
Required
type- "command" or "prompt"commandorprompt- Script path or prompt text
Optional
timeout- Max execution time in ms (default: 60000)continue- Continue after hook? (default: true)stopReason- Message when continue=falsesuppressOutput- Hide stdout from transcript (default: false)systemMessage- Warning message to user
---
Environment Variables
Available in all hooks:
$CLAUDE_PROJECT_DIR- Project root directory$CLAUDE_CURRENT_DIR- Current working directory$SESSION_ID- Unique session identifier$CLAUDE_PLUGIN_ROOT- Hook installation directory$CLAUDE_ENV_FILE- File for persisting environment vars
Example:
echo "Project: $CLAUDE_PROJECT_DIR" >&2
echo "Session: $SESSION_ID" >&2---
JSON Input Structure
Standard input structure for hooks:
{
"session_id": "abc123",
"transcript_path": "/path/to/transcript",
"current_dir": "/path/to/current",
"input": {
// Tool-specific fields
}
}Tool-specific fields:
file_path- Read, Write, Editcommand- Bashold_string,new_string- Editcontent- Write
---
Common jq Patterns
Extract fields safely with fallbacks:
# Extract with default empty string
FILE=$(echo "$INPUT" | jq -r '.input.file_path // empty')
# Extract array elements
FILES=$(echo "$INPUT" | jq -r '.input.files[]')
# Check if field exists
if echo "$INPUT" | jq -e '.input.file_path' >/dev/null; then
# Field exists
fi
# Parse entire object
INPUT_OBJ=$(echo "$INPUT" | jq '.input')
# Extract multiple fields
eval $(echo "$INPUT" | jq -r '@sh "FILE=\(.input.file_path) CMD=\(.input.command)"')---
Common Bash Patterns
Validate File in Project
if [[ "$FILE" != "$CLAUDE_PROJECT_DIR"* ]]; then
echo "File outside project" >&2
exit 2
fiCheck Tool Exists
if ! command -v prettier &> /dev/null; then
echo "prettier not installed" >&2
exit 0 # Skip gracefully
fiFile Extension Matching
case "$FILE" in
*.ts|*.tsx)
# TypeScript files
;;
*.py)
# Python files
;;
*)
exit 0 # Skip other files
;;
esacBackground Execution
# Run heavy operation in background
(heavy_operation "$FILE" &)
exit 0 # Return immediatelyLogging
# Log to stderr (shows in transcript)
echo "Error: something failed" >&2
# Log to file (for debugging)
LOG_FILE=~/.claude-hooks/debug.log
echo "[$(date)] Debug info" >> "$LOG_FILE"---
Hook Events
PreToolUse- Before tool executes (can block)PostToolUse- After tool completes (cannot block)UserPromptSubmit- Before processing user inputSessionStart- When Claude Code startsSessionEnd- When Claude Code exitsNotification- During alertsStop/SubagentStop- When responses finishPreCompact- Before context compaction
---
Common Matchers
"Write"- File writes only"Edit"- File edits only"Edit|Write"- All file modifications"Bash"- Shell commands"Read"- File reads"*"- All tools (use sparingly)"mcp__*"- All MCP tools"mcp__github__*"- Specific MCP provider
---
Security Patterns
Block Sensitive Files
BLOCKED=(".env" "*.key" "*secret*")
for pattern in "${BLOCKED[@]}"; do
if [[ "$FILE" == $pattern ]]; then
echo "Blocked: $FILE" >&2
exit 2
fi
doneSanitize Path
# No directory traversal
[[ "$FILE" != *".."* ]] || exit 2
# Must be in project
[[ "$FILE" == "$CLAUDE_PROJECT_DIR"* ]] || exit 2Quote Variables
# Always quote file paths
cat "$FILE" # NOT: cat $FILE
prettier --write "$FILE" # NOT: prettier --write $FILE---
Performance Tips
1. Keep PreToolUse < 100ms 2. Use specific matchers (not *) 3. Run heavy ops in background 4. Cache expensive results 5. Check cheap conditions first 6. Use built-in bash when possible
---
Common Commands
# Check if tool installed
command -v toolname &> /dev/null
# Check if file exists
[[ -f "$FILE" ]]
# Check if directory exists
[[ -d "$DIR" ]]
# Get file extension
ext="${FILE##*.}"
# Get filename without extension
name="${FILE%.*}"
# Get directory of file
dir="$(dirname "$FILE")"
# Get basename
base="$(basename "$FILE")"Reliability & Performance Requirements
Hooks must be reliable (work 100% of the time) and performant (don't block Claude).
---
Reliability Requirements
Handle Missing Dependencies
Never assume tools are installed:
# Check tool exists before using
if ! command -v prettier &> /dev/null; then
echo "prettier not installed, skipping formatting" >&2
exit 0 # Success exit (just skip)
fi
# Now safe to use
prettier --write "$FILE"Common missing tools:
prettier,black,gofmt(formatters)jq(JSON parsing - though usually installed)git(version control - might be missing)- Language-specific tools (
npm,cargo,go)
Best practice: Exit 0 (success) when tool is missing, not exit 1 (error). The hook simply doesn't run.
---
Check File Exists
Don't assume files exist:
# Check file exists before reading
if [[ ! -f "$FILE" ]]; then
echo "File not found: $FILE" >&2
exit 1
fi
# Now safe to read
content=$(cat "$FILE")Edge cases:
- File deleted between tool invocation and hook execution
- File path typo in input
- Network drives that disconnect
---
Set Timeouts
Default timeout is 60 seconds. For slow operations, set explicit timeout:
{
"hooks": [{
"type": "command",
"command": "./slow-operation.sh",
"timeout": 10000 // 10 seconds (in milliseconds)
}]
}Guidelines:
- PreToolUse hooks: 1-5 seconds max
- PostToolUse hooks: 5-10 seconds max
- SessionStart/End: 30 seconds max
For longer operations, run in background:
# Don't block Claude for 30+ seconds
(heavy_operation "$FILE" &)
exit 0 # Return immediately---
Log Errors Properly
Log to stderr or file, not stdout:
LOG_FILE=~/.claude-hooks/my-hook.log
# Log to stderr (shown in transcript with Ctrl-R)
echo "Hook failed: $reason" >&2
# Or log to file (for debugging)
mkdir -p "$(dirname "$LOG_FILE")"
echo "[$(date)] Error: $reason" >> "$LOG_FILE"Don't log to stdout unless you want output in Claude's conversational transcript.
Logging levels:
- stderr: Errors, warnings, important events
- Log file: Debugging info, verbose output
- stdout: Success messages (shown to user)
---
Test With Edge Cases
Always test with these scenarios:
Edge Case Files
# Files with spaces
"my file.txt"
# Unicode filenames
"文件.txt"
"файл.txt"
# Deep nested paths
"src/components/features/auth/login/LoginForm.tsx"
# Absolute paths
"/tmp/test.txt"
"/Users/name/Documents/file.txt"
# Paths with special chars
"file (1).txt"
"file-v2.0.txt"
"file@2024.txt"Edge Case Input
# Malformed JSON
echo 'not json' | ./hook.sh
# Missing fields
echo '{"input":{}}' | ./hook.sh
# Empty strings
echo '{"input":{"file_path":""}}' | ./hook.sh
# null values
echo '{"input":{"file_path":null}}' | ./hook.sh
# Nested objects
echo '{"input":{"nested":{"file":"test.txt"}}}' | ./hook.sh---
Performance Requirements
Keep Hooks Fast
Target latency:
- PreToolUse: < 100ms (blocks tool execution)
- PostToolUse: < 500ms (blocks next action)
- SessionStart: < 1000ms (one-time delay)
Slow operations to avoid in PreToolUse:
- Running test suites (use PostToolUse + background)
- Full project linting (lint only changed file)
- Network calls (API requests, webhooks)
- Heavy file I/O (reading large files)
Example: Slow vs Fast
# ❌ SLOW - type checks entire project
tsc --noEmit # 5-10 seconds
# ✅ FAST - only check changed file
tsc --noEmit "$FILE" # 100-500ms---
Use Specific Matchers
Broad matchers (*) trigger hooks unnecessarily:
// ❌ BAD - runs on EVERY tool call
{
"matcher": "*",
"hooks": [{...}]
}
// ✅ GOOD - only file writes
{
"matcher": "Write",
"hooks": [{...}]
}
// ✅ BETTER - only specific tools
{
"matcher": "Edit|Write", // File modifications only
"hooks": [{...}]
}
// ✅ BEST - filter by file extension in hook
{
"matcher": "Write",
"hooks": [{
"command": "./format-typescript.sh" // Checks .ts/.tsx inside
}]
}Performance impact:
matcher: "*"on 100 tool calls = 100 hook executionsmatcher: "Write"on 100 tool calls (10 writes) = 10 hook executions
---
Dedupe Expensive Operations
If multiple hooks match, they run in parallel. Avoid duplicate work:
# Use lock file to prevent parallel execution
LOCK_FILE="/tmp/claude-hook-${SESSION_ID}-${HOOK_NAME}.lock"
if [[ -f "$LOCK_FILE" ]]; then
echo "Hook already running, skipping" >&2
exit 0
fi
touch "$LOCK_FILE"
trap "rm -f '$LOCK_FILE'" EXIT # Clean up on exit
# Do work here (only one instance runs)
expensive_operationWhen to use: Hooks that take >1 second and might run in parallel.
---
Cache Results
For expensive checks, cache by file hash:
CACHE_DIR=~/.claude-hooks/cache
mkdir -p "$CACHE_DIR"
# Compute file hash
FILE_HASH=$(shasum "$FILE" | cut -d' ' -f1)
CACHE_FILE="$CACHE_DIR/lint-$FILE_HASH"
# Check cache
if [[ -f "$CACHE_FILE" ]]; then
# File unchanged, use cached result
cat "$CACHE_FILE"
exit 0
fi
# Run expensive operation
result=$(eslint "$FILE")
# Cache result
echo "$result" > "$CACHE_FILE"
echo "$result"Good for: Linting, type checking, complex validation
---
Run Heavy Operations in Background
Don't block Claude for slow operations:
# ❌ BLOCKS Claude for 30 seconds
npm test
npm run build
# ✅ RUN IN BACKGROUND - returns immediately
(npm test > /tmp/test-results.log 2>&1 &)
(npm run build > /tmp/build.log 2>&1 &)
exit 0 # Hook completes instantlyBackground operation pattern:
# Run in background with logging
(
# Subshell runs independently
sleep 2
result=$(expensive_operation "$FILE" 2>&1)
echo "[$(date)] Result: $result" >> ~/.claude-hooks/async.log
) &
# Hook returns immediately
exit 0When to use: Tests, builds, network calls, heavy linting
---
Reliability Checklist
Before deploying:
- [ ] Checks if required tools are installed (
command -v) - [ ] Checks if files exist before reading (
[[ -f "$FILE" ]]) - [ ] Sets reasonable timeout (< 5s for PreToolUse)
- [ ] Logs errors to stderr or file, not stdout
- [ ] Handles missing JSON fields gracefully
- [ ] Handles malformed JSON without crashing
- [ ] Handles Unicode filenames
- [ ] Handles filenames with spaces
- [ ] Handles deep nested paths
- [ ] Fails gracefully on errors (doesn't crash)
---
Performance Checklist
Before deploying:
- [ ] Hook completes in < 100ms for PreToolUse
- [ ] Hook completes in < 500ms for PostToolUse
- [ ] Uses specific matchers (not
*unless necessary) - [ ] Runs heavy operations in background
- [ ] Caches expensive results by file hash
- [ ] Dedupe with locks if hook might run in parallel
- [ ] Only processes relevant files (check extension)
- [ ] No unnecessary file reads or writes
---
Testing Reliability & Performance
Test Reliability
#!/bin/bash
# reliability-test.sh
echo "=== Testing Missing Tool ==="
PATH="/usr/bin" ./hook.sh # Remove tool from PATH
# Expected: Exit 0, log "tool not installed"
echo "=== Testing Missing File ==="
echo '{"input":{"file_path":"/nonexistent.txt"}}' | ./hook.sh
# Expected: Exit 1, log "file not found"
echo "=== Testing Malformed JSON ==="
echo 'not json' | ./hook.sh
# Expected: Exit 1, log "JSON parse failed"
echo "=== Testing Empty Input ==="
echo '{}' | ./hook.sh
# Expected: Exit 0 or 1, no crash
echo "All reliability tests passed"Test Performance
#!/bin/bash
# performance-test.sh
echo "=== Testing Hook Speed ==="
# Measure execution time
start=$(date +%s%N)
echo '{"input":{"file_path":"test.ts"}}' | ./hook.sh
end=$(date +%s%N)
duration_ms=$(( (end - start) / 1000000 ))
echo "Hook took ${duration_ms}ms"
# Verify within limits
if [[ $duration_ms -lt 100 ]]; then
echo "✅ Performance PASS (< 100ms)"
else
echo "❌ Performance FAIL (>= 100ms)"
fi---
Performance Optimization Tips
1. Profile your hook: Use time command to measure performance
time echo '{"input":{"file_path":"test.ts"}}' | ./hook.sh2. Avoid spawning processes: Each command -v, jq, etc. spawns a process
# Slow - spawns jq 3 times
FILE=$(echo "$INPUT" | jq -r '.input.file_path')
DIR=$(echo "$INPUT" | jq -r '.input.dir')
CMD=$(echo "$INPUT" | jq -r '.input.command')
# Fast - single jq call
eval $(echo "$INPUT" | jq -r '@sh "FILE=\(.input.file_path) DIR=\(.input.dir) CMD=\(.input.command)"')3. Use built-in bash instead of external tools when possible
# Slow - spawns grep
if echo "$FILE" | grep -q ".ts$"; then
# Fast - bash pattern matching
if [[ "$FILE" == *.ts ]]; then4. Lazy evaluation: Only do expensive work when necessary
# Check cheap conditions first
[[ -f "$FILE" ]] || exit 0
[[ "$FILE" == *.ts ]] || exit 0
# Only now run expensive check
result=$(expensive_operation "$FILE")Security Requirements
Hooks execute automatically with user permissions and can read, modify, or delete any file the user can access. Security is non-negotiable.
---
MUST-HAVE Security Checks
Every hook must implement these security measures:
1. Input Validation
Always validate JSON input before using it:
#!/bin/bash
set -euo pipefail # Exit on errors, undefined vars
INPUT=$(cat)
# Validate JSON parse
if ! FILE=$(echo "$INPUT" | jq -r '.input.file_path // empty' 2>&1); then
echo "JSON parse failed: $FILE" >&2
exit 1
fi
# Validate field exists
if [[ -z "$FILE" ]]; then
echo "No file path in input" >&2
exit 1
fiWhy this matters: Malformed JSON or missing fields will crash hooks. Always validate before using.
---
2. Path Sanitization
Validate files are in the project directory:
# Validate file is in project
if [[ "$FILE" != "$CLAUDE_PROJECT_DIR"* ]]; then
echo "File outside project: $FILE" >&2
exit 2 # Block operation
fi
# Validate no directory traversal
if [[ "$FILE" == *".."* ]]; then
echo "Path traversal detected: $FILE" >&2
exit 2
fiCommon attacks prevented:
../../../etc/passwd(directory traversal)/etc/shadow(absolute path outside project)~/.ssh/id_rsa(home directory access)
---
3. Sensitive File Protection
Block operations on sensitive files:
# Block list (extend as needed)
BLOCKED_PATTERNS=(
".env"
".env.*"
"*.pem"
"*.key"
"*credentials*"
"*secret*"
".git/*"
".ssh/*"
)
for pattern in "${BLOCKED_PATTERNS[@]}"; do
if [[ "$FILE" == $pattern ]]; then
echo "Blocked: $FILE matches sensitive pattern $pattern" >&2
exit 2
fi
doneFiles to always block:
.env,.env.local,.env.production(environment secrets)*.pem,*.key(private keys)*credentials*,*secret*(anything with credentials/secrets in name).git/*(Git internals).ssh/*(SSH keys)
---
4. Quote All Variables
Spaces and special characters in paths break unquoted variables:
# ❌ WRONG - breaks on spaces
cat $FILE # Fails on "my file.txt"
prettier --write $FILE # Fails with spaces
rm $FILE # DANGEROUS - could delete wrong files
# ✅ RIGHT - handles spaces and special chars
cat "$FILE" # Handles spaces
prettier --write "$FILE" # Safe
rm "$FILE" # Scoped to exact fileTest with these filenames:
"my file.txt"(spaces)"文件.txt"(Unicode)"file (1).txt"(special chars)
---
5. Use Absolute Paths for Scripts
Relative paths might not resolve correctly:
# ❌ WRONG - relative path might not work
./my-script.sh
# ✅ RIGHT - explicit absolute path
"${CLAUDE_PLUGIN_ROOT}/scripts/my-script.sh"
# ✅ ALSO RIGHT - use environment variable
"$HOME/.claude/scripts/my-script.sh"Why: Hook execution directory might not be where you expect. Always use absolute paths.
---
Security Checklist
Before deploying a hook, verify:
- [ ] All JSON input is validated before use
- [ ] All file paths are validated to be in
$CLAUDE_PROJECT_DIR - [ ] Path traversal attempts (
..) are blocked - [ ] Sensitive files (
.env,*.key, etc.) are blocked - [ ] All variables containing paths are quoted
- [ ] All script paths are absolute
- [ ] No shell injection vulnerabilities (careful with
eval,bash -c) - [ ] No command injection via unsanitized input
---
Common Security Vulnerabilities
Vulnerability #1: Command Injection
# ❌ DANGEROUS - command injection
COMMAND=$(echo "$INPUT" | jq -r '.input.command')
eval "$COMMAND" # Attacker can run arbitrary code
# ✅ SAFE - validate and sanitize
COMMAND=$(echo "$INPUT" | jq -r '.input.command // empty')
# Only allow specific commands
case "$COMMAND" in
"npm test"|"npm build")
$COMMAND
;;
*)
echo "Command not allowed: $COMMAND" >&2
exit 2
;;
esacVulnerability #2: Path Traversal
# ❌ VULNERABLE - no path validation
FILE=$(echo "$INPUT" | jq -r '.input.file_path')
rm "$FILE" # Could delete ../../../etc/passwd
# ✅ SAFE - validate path first
FILE=$(echo "$INPUT" | jq -r '.input.file_path // empty')
[[ "$FILE" == "$CLAUDE_PROJECT_DIR"* ]] || exit 2
[[ "$FILE" != *".."* ]] || exit 2
rm "$FILE"Vulnerability #3: Trusting User Input
# ❌ DANGEROUS - trust user input
MESSAGE=$(echo "$INPUT" | jq -r '.input.message')
echo "$MESSAGE" | mail -s "Alert" admin@example.com
# ✅ SAFE - sanitize and validate
MESSAGE=$(echo "$INPUT" | jq -r '.input.message // empty')
# Remove special chars
MESSAGE=$(echo "$MESSAGE" | tr -cd '[:alnum:][:space:].')
# Limit length
MESSAGE="${MESSAGE:0:200}"
echo "$MESSAGE" | mail -s "Alert" admin@example.com---
Security Best Practices
Principle of Least Privilege
Only access what you need:
# Don't read entire files if you only need metadata
# ❌ Unnecessary file read
content=$(cat "$FILE")
# ✅ Better - just check if exists
[[ -f "$FILE" ]] && echo "File exists"Fail Closed on Security Checks
When in doubt, block:
# If validation fails, block rather than allow
if ! validate_path "$FILE"; then
echo "Path validation failed, blocking" >&2
exit 2 # Block
fi
# Don't continue on validation errorsLog Security Events
Log blocked operations for audit:
LOG_FILE=~/.claude-hooks/security.log
if [[ "$FILE" == ".env" ]]; then
echo "[$(date)] BLOCKED: Attempt to modify .env" >> "$LOG_FILE"
echo " File: $FILE" >> "$LOG_FILE"
echo " Session: $SESSION_ID" >> "$LOG_FILE"
exit 2
fi---
Testing Security
Test Malicious Input
# Test path traversal
echo '{"input":{"file_path":"../../../etc/passwd"}}' | ./my-hook.sh
# Expected: Exit 2 (blocked)
# Test sensitive file
echo '{"input":{"file_path":".env"}}' | ./my-hook.sh
# Expected: Exit 2 (blocked)
# Test absolute path outside project
echo '{"input":{"file_path":"/etc/shadow"}}' | ./my-hook.sh
# Expected: Exit 2 (blocked)
# Test malformed JSON
echo 'not json' | ./my-hook.sh
# Expected: Exit 1 (error logged)Test Edge Cases
# File with spaces
echo '{"input":{"file_path":"my file.txt"}}' | ./my-hook.sh
# Unicode filename
echo '{"input":{"file_path":"文件.txt"}}' | ./my-hook.sh
# Special characters
echo '{"input":{"file_path":"file (1).txt"}}' | ./my-hook.sh---
Security Resources
Testing & Debugging Hooks
Guide for testing hooks thoroughly and debugging when things go wrong.
---
Manual Testing
Create test input and verify behavior:
# Test with sample JSON
echo '{
"session_id": "test",
"input": {
"file_path": "/tmp/test.ts"
}
}' | ./my-hook.sh
# Check exit code
echo $? # 0 = success, 2 = blocked, 1 = error---
Edge Case Testing
Always test with these scenarios:
#!/bin/bash
# test-hook.sh
HOOK=./my-hook.sh
test_case() {
local description="$1"
local input="$2"
local expected_exit="$3"
echo "Testing: $description"
echo "$input" | $HOOK
actual_exit=$?
if [[ $actual_exit -eq $expected_exit ]]; then
echo " PASS"
else
echo " FAIL (expected exit $expected_exit, got $actual_exit)"
return 1
fi
}
# Test cases
test_case "Normal file" \
'{"input":{"file_path":"/tmp/test.ts"}}' \
0
test_case "Sensitive .env file" \
'{"input":{"file_path":".env"}}' \
2
test_case "File with spaces" \
'{"input":{"file_path":"/tmp/my file.ts"}}' \
0
test_case "Missing file_path" \
'{"input":{}}' \
1
test_case "Malformed JSON" \
'not json' \
1
echo "All tests passed"---
Integration Testing
1. Register hook in Claude Code 2. Trigger the event (write file, run command) 3. Check transcript (Ctrl-R) for hook output 4. Verify expected behavior
---
Debugging Techniques
Enable Verbose Logging
#!/bin/bash
set -x # Print commands as they executeCheck Transcript
Run Claude Code with Ctrl-R (transcript mode):
PreToolUse hook: ./my-hook.sh
stdout: Formatted file.ts
stderr:
exit: 0
duration: 47msTest JSON Parsing
# Debug what jq extracts
INPUT=$(cat)
echo "$INPUT" | jq '.' >&2 # Show full JSON
echo "$INPUT" | jq -r '.input.file_path' >&2 # Show fieldCheck Environment Variables
echo "PROJECT_DIR: $CLAUDE_PROJECT_DIR" >&2
echo "CURRENT_DIR: $CLAUDE_CURRENT_DIR" >&2
echo "SESSION_ID: $SESSION_ID" >&2
echo "PLUGIN_ROOT: $CLAUDE_PLUGIN_ROOT" >&2---
Common Debugging Issues
Issue: Hook Not Running
Check:
- Is hook registered in hook.json?
- Does matcher match the tool? (
"matcher": "Write") - Is script executable? (
chmod +x hook.sh) - Is script path absolute? (
${CLAUDE_PLUGIN_ROOT}/hook.sh)
Issue: JSON Parse Errors
Check:
- Is input valid JSON? (test with
jq '.') - Are you using
// emptyfallback? (jq -r '.field // empty') - Are you checking if field exists before using?
Issue: Hook Times Out
Check:
- Is timeout set high enough? (default 60s)
- Is hook doing expensive operations? (run in background)
- Is hook hanging on user input? (use non-interactive tools)
Issue: Files Not Found
Check:
- Is path absolute or relative?
- Does file exist? (
[[ -f "$FILE" ]]) - Are you in correct directory? (
cd "$CLAUDE_PROJECT_DIR")
---
Performance Profiling
Measure hook execution time:
# Time the hook
time echo '{"input":{"file_path":"test.ts"}}' | ./hook.sh
# Output:
# real 0m0.047s # Total time (47ms)
# user 0m0.023s # CPU time
# sys 0m0.015s # System timeTarget: < 100ms for PreToolUse hooks
---
Test Automation
Create automated test suite:
#!/bin/bash
# run-all-tests.sh
TESTS_PASSED=0
TESTS_FAILED=0
run_test() {
if "$@"; then
((TESTS_PASSED++))
else
((TESTS_FAILED++))
fi
}
run_test ./test-normal-files.sh
run_test ./test-sensitive-files.sh
run_test ./test-edge-cases.sh
run_test ./test-performance.sh
echo "---"
echo "Tests passed: $TESTS_PASSED"
echo "Tests failed: $TESTS_FAILED"
[[ $TESTS_FAILED -eq 0 ]] || exit 1Related skills
How it compares
Use claude-hook-writer for Claude Code hook lifecycle work; use a general shell scripting skill for tasks outside the Claude hook event model.
FAQ
What Claude Code events does claude-hook-writer cover?
claude-hook-writer guides PreToolUse and PostToolUse hook design, including matchers, required inputs, command versus prompt hook types, and correct exit codes. It also documents testing, debugging, and publishing workflows.
What references ship with claude-hook-writer?
claude-hook-writer bundles six reference files: security requirements, reliability and performance, code templates, testing and debugging, publishing guide, and quick-reference syntax for exit codes, jq patterns, and environment variables.
What version is claude-hook-writer?
claude-hook-writer metadata lists version 2.0.0, last verified 2025-12-17, with about 55% token savings from optimization and guidance that prevents five documented common hook pitfalls.