Create and configure Claude Code hooks with reference documentation and interactive guidance.
Create and configure Claude Code hooks with interactive guidance and production-ready templates for common tools like ESLint, Prettier, and TypeScript.
/plugin marketplace add alexanderop/claude-code-builder/plugin install claude-code-builder@claude-code-builderCreate and configure Claude Code hooks with reference documentation and interactive guidance.
Inputs:
$1 — HOOK_TYPE (optional: PreToolUse, PostToolUse, UserPromptSubmit, Notification, Stop, SubagentStop, PreCompact, SessionStart, SessionEnd)$2 — MATCHER (optional: tool name pattern, e.g., "Bash", "Edit|Write", "*")$3 — COMMAND (optional: shell command to execute)Outputs: STATUS=<OK|FAIL> HOOK_FILE=<path>
Detect common use cases (hybrid approach):
.claude/hooks/ + settings.json entryDetermine hook configuration mode:
Validate inputs:
.claude/hooks/ directory existsReference documentation: Review the Claude Code hooks documentation for best practices and examples:
Hook Events:
PreToolUse: Runs before tool calls (can block them)PostToolUse: Runs after tool calls completeUserPromptSubmit: Runs when user submits a promptNotification: Runs when Claude Code sends notificationsStop: Runs when Claude Code finishes respondingSubagentStop: Runs when subagent tasks completePreCompact: Runs before compact operationsSessionStart: Runs when session starts/resumesSessionEnd: Runs when session endsProduction-Ready Script Templates:
When user requests common integrations, generate these external scripts in .claude/hooks/:
ESLint Auto-Fix (run-eslint.sh):
#!/usr/bin/env bash
# Auto-fix JavaScript/TypeScript files with ESLint after edits
set -euo pipefail
# Extract file path from Claude's JSON payload
file_path="$(jq -r '.tool_input.file_path // ""')"
# Only process JS/TS files
[[ "$file_path" =~ \.(js|jsx|ts|tsx)$ ]] || exit 0
# Auto-detect package manager (prefer project's lock file)
if command -v pnpm >/dev/null 2>&1 && [ -f "pnpm-lock.yaml" ]; then
PM="pnpm exec"
elif command -v yarn >/dev/null 2>&1 && [ -f "yarn.lock" ]; then
PM="yarn"
else
PM="npx"
fi
# Run ESLint with auto-fix from project root
cd "$CLAUDE_PROJECT_DIR" && $PM eslint --fix "$file_path"
Prettier Format (run-prettier.sh):
#!/usr/bin/env bash
# Format files with Prettier after edits
set -euo pipefail
file_path="$(jq -r '.tool_input.file_path // ""')"
# Skip non-formattable files
[[ "$file_path" =~ \.(js|jsx|ts|tsx|json|css|scss|md|html|yml|yaml)$ ]] || exit 0
# Auto-detect package manager
if command -v pnpm >/dev/null 2>&1 && [ -f "pnpm-lock.yaml" ]; then
PM="pnpm exec"
elif command -v yarn >/dev/null 2>&1 && [ -f "yarn.lock" ]; then
PM="yarn"
else
PM="npx"
fi
cd "$CLAUDE_PROJECT_DIR" && $PM prettier --write "$file_path"
TypeScript Type Check (run-typescript.sh):
#!/usr/bin/env bash
# Run TypeScript type checker on TS/TSX file edits
set -euo pipefail
file_path="$(jq -r '.tool_input.file_path // ""')"
# Only process TypeScript files
[[ "$file_path" =~ \.(ts|tsx)$ ]] || exit 0
# Auto-detect package manager
if command -v pnpm >/dev/null 2>&1 && [ -f "pnpm-lock.yaml" ]; then
PM="pnpm exec"
elif command -v yarn >/dev/null 2>&1 && [ -f "yarn.lock" ]; then
PM="yarn"
else
PM="npx"
fi
# Run tsc --noEmit to check types without emitting files
cd "$CLAUDE_PROJECT_DIR" && $PM tsc --noEmit --pretty
Run Affected Tests (run-tests.sh):
#!/usr/bin/env bash
# Run tests for modified files
set -euo pipefail
file_path="$(jq -r '.tool_input.file_path // ""')"
# Only run tests for source files (not test files themselves)
[[ "$file_path" =~ \.(test|spec)\.(js|ts|jsx|tsx)$ ]] && exit 0
[[ "$file_path" =~ \.(js|jsx|ts|tsx)$ ]] || exit 0
# Auto-detect test runner and package manager
if [ -f "vitest.config.ts" ] || [ -f "vitest.config.js" ]; then
TEST_CMD="vitest related --run"
elif [ -f "jest.config.js" ] || [ -f "jest.config.ts" ]; then
TEST_CMD="jest --findRelatedTests"
else
exit 0 # No test runner configured
fi
if command -v pnpm >/dev/null 2>&1 && [ -f "pnpm-lock.yaml" ]; then
PM="pnpm exec"
elif command -v yarn >/dev/null 2>&1 && [ -f "yarn.lock" ]; then
PM="yarn"
else
PM="npx"
fi
cd "$CLAUDE_PROJECT_DIR" && $PM $TEST_CMD "$file_path"
Commit Message Validation (validate-commit.sh):
#!/usr/bin/env bash
# Validate commit messages follow conventional commits format
set -euo pipefail
# Extract commit message from tool input
commit_msg="$(jq -r '.tool_input // ""')"
# Check for conventional commit format: type(scope): message
if ! echo "$commit_msg" | grep -qE '^(feat|fix|docs|style|refactor|perf|test|chore|ci|build|revert)(\(.+\))?: .+'; then
echo "ERROR: Commit message must follow conventional commits format"
echo "Expected: type(scope): description"
echo "Got: $commit_msg"
exit 2 # Block the commit
fi
Corresponding settings.json entries:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"comment": "Auto-fix with ESLint",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/run-eslint.sh"
}
]
}
]
}
}
Best Practices for Hook Development:
Always use shell safety headers:
#!/usr/bin/env bash
set -euo pipefail # Exit on error, undefined vars, pipe failures
Extract data from Claude's JSON payload:
# File path (Edit/Write tools)
file_path="$(jq -r '.tool_input.file_path // ""')"
# Command (Bash tool)
command="$(jq -r '.tool_input.command // ""')"
# Tool name
tool_name="$(jq -r '.tool_name // ""')"
# Full tool input
tool_input="$(jq -r '.tool_input' | jq -c .)"
Use environment variables provided by Claude:
$CLAUDE_PROJECT_DIR # Project root directory
$CLAUDE_USER_DIR # User's ~/.claude directory
$CLAUDE_SESSION_ID # Current session identifier
Efficient file extension filtering:
# Good: Use bash regex matching
[[ "$file_path" =~ \.(ts|tsx)$ ]] || exit 0
# Avoid: Spawning grep subprocess
echo "$file_path" | grep -q '\.ts$' || exit 0
Package manager auto-detection pattern:
# Check lock files to match project's package manager
if command -v pnpm >/dev/null 2>&1 && [ -f "pnpm-lock.yaml" ]; then
PM="pnpm exec"
elif command -v yarn >/dev/null 2>&1 && [ -f "yarn.lock" ]; then
PM="yarn"
else
PM="npx" # Fallback to npx
fi
Exit codes matter:
exit 0 # Success: Allow operation to continue
exit 1 # Error: Log error but don't block
exit 2 # Block: Prevent operation in PreToolUse hooks
Performance considerations:
When to use external scripts vs inline commands:
.claude/hooks/*.sh): Complex logic, multiple steps, reusable patternsExample inline command:
"command": "jq -r '.tool_input.command' >> ~/.claude/command-log.txt"
Common use cases and examples (updated with best practices):
Logging Bash commands:
{
"hooks": {
"PreToolUse": [{
"matcher": "Bash",
"hooks": [{
"type": "command",
"comment": "Log all Bash commands with descriptions",
"command": "set -euo pipefail; cmd=$(jq -r '.tool_input.command // \"\"'); desc=$(jq -r '.tool_input.description // \"No description\"'); echo \"[$(date -u +%Y-%m-%dT%H:%M:%SZ)] $cmd - $desc\" >> \"$CLAUDE_USER_DIR/bash-command-log.txt\""
}]
}]
}
}
Auto-format TypeScript files with Prettier:
{
"hooks": {
"PostToolUse": [{
"matcher": "Edit|Write",
"hooks": [{
"type": "command",
"comment": "Auto-format TypeScript files after edits",
"command": "set -euo pipefail; file_path=$(jq -r '.tool_input.file_path // \"\"'); [[ \"$file_path\" =~ \\.(ts|tsx)$ ]] || exit 0; cd \"$CLAUDE_PROJECT_DIR\" && npx prettier --write \"$file_path\""
}]
}]
}
}
Block sensitive file edits:
{
"hooks": {
"PreToolUse": [{
"matcher": "Edit|Write",
"hooks": [{
"type": "command",
"comment": "Prevent edits to sensitive files",
"command": "set -euo pipefail; file_path=$(jq -r '.tool_input.file_path // \"\"'); if [[ \"$file_path\" =~ \\.(env|secrets|credentials) ]] || [[ \"$file_path\" == *\"package-lock.json\" ]] || [[ \"$file_path\" == *.git/* ]]; then echo \"ERROR: Cannot edit sensitive file: $file_path\" >&2; exit 2; fi"
}]
}]
}
}
Desktop notifications:
{
"hooks": {
"Notification": [{
"matcher": "*",
"hooks": [{
"type": "command",
"comment": "Send desktop notifications",
"command": "if command -v notify-send >/dev/null 2>&1; then notify-send 'Claude Code' 'Awaiting your input'; elif command -v osascript >/dev/null 2>&1; then osascript -e 'display notification \"Awaiting your input\" with title \"Claude Code\"'; fi"
}]
}]
}
}
Generic Patterns Reference:
Pattern: Extract file path and check multiple extensions
set -euo pipefail
file_path=$(jq -r '.tool_input.file_path // ""')
[[ "$file_path" =~ \.(js|ts|jsx|tsx|py|go|rs)$ ]] || exit 0
# Your command here
Pattern: Process multiple files from array
set -euo pipefail
jq -r '.tool_input.files[]?' | while IFS= read -r file; do
[[ -f "$file" ]] && echo "Processing: $file"
# Your processing logic here
done
Pattern: Conditional execution based on directory
set -euo pipefail
file_path=$(jq -r '.tool_input.file_path // ""')
# Only process files in src/ directory
[[ "$file_path" =~ ^src/ ]] || exit 0
# Your command here
Pattern: Extract and validate Bash command
set -euo pipefail
cmd=$(jq -r '.tool_input.command // ""')
# Block dangerous commands
if [[ "$cmd" =~ (rm -rf|mkfs|dd|:(){:|:&};:) ]]; then
echo "ERROR: Dangerous command blocked" >&2
exit 2
fi
Pattern: Background/async execution
set -euo pipefail
file_path=$(jq -r '.tool_input.file_path // ""')
# Run slow operation in background, don't block Claude
(cd "$CLAUDE_PROJECT_DIR" && npm run build "$file_path" &> /tmp/build.log) &
Pattern: Conditional tool execution
set -euo pipefail
tool_name=$(jq -r '.tool_name // ""')
case "$tool_name" in
Edit|Write)
file_path=$(jq -r '.tool_input.file_path // ""')
echo "File modified: $file_path"
;;
Bash)
cmd=$(jq -r '.tool_input.command // ""')
echo "Command executed: $cmd"
;;
esac
Pattern: Multi-tool chain with error handling
set -euo pipefail
file_path=$(jq -r '.tool_input.file_path // ""')
[[ "$file_path" =~ \.(ts|tsx)$ ]] || exit 0
cd "$CLAUDE_PROJECT_DIR"
# Run linter
if ! npx eslint --fix "$file_path" 2>/dev/null; then
echo "Warning: ESLint failed" >&2
fi
# Run formatter (always runs even if linter fails)
npx prettier --write "$file_path" 2>/dev/null || true
Pattern: Cache validation results
set -euo pipefail
file_path=$(jq -r '.tool_input.file_path // ""')
cache_file="/tmp/claude-hook-cache-$(echo \"$file_path\" | md5sum | cut -d' ' -f1)"
# Check cache freshness
if [[ -f "$cache_file" ]] && [[ "$cache_file" -nt "$file_path" ]]; then
cat "$cache_file"
exit 0
fi
# Run expensive validation
result=$(npx tsc --noEmit "$file_path" 2>&1)
echo "$result" > "$cache_file"
echo "$result"
Pattern: Cross-platform compatibility
set -euo pipefail
# Detect OS and use appropriate commands
case "$(uname -s)" in
Darwin*)
# macOS
osascript -e 'display notification "Build complete"'
;;
Linux*)
# Linux
notify-send "Build complete"
;;
MINGW*|MSYS*|CYGWIN*)
# Windows
powershell -Command "New-BurntToastNotification -Text 'Build complete'"
;;
esac
Security considerations:
Hook creation workflow:
For common tools (ESLint, Prettier, TypeScript, Tests):
.claude/hooks/ directory if neededrun-eslint.sh)chmod +x)$CLAUDE_PROJECT_DIR referenceFor custom hooks:
Determine appropriate hook event for the use case
Define matcher pattern (specific tool name, regex like "Edit|Write", or "*" for all)
Write shell command that processes JSON input via stdin
Always start with set -euo pipefail for safety
Use $CLAUDE_PROJECT_DIR and other environment variables
Test hook command independently before registering
Choose storage location:
.claude/settings.json - Project-specific, committed to git.claude/settings.local.json - Project-specific, not committed~/.claude/settings.json - User-wide, all projectsRegister hook and reload with /hooks command
Debugging hooks:
claude --debug to see hook execution logs/hooks command to verify hook registrationecho '{"tool_name":"Edit","tool_input":{"file_path":"test.ts"}}' | .claude/hooks/run-eslint.sh
jq to inspect JSON data passed to hookschmod +x for external scripts)$CLAUDE_PROJECT_DIR resolves correctlyTemplate mode:
.claude/hooks/[script-name].shSTATUS=OK HOOK_FILE=.claude/hooks/[script-name].sh SETTINGS=.claude/settings.jsonInteractive/Direct mode:
STATUS=OK HOOK_FILE=~/.claude/settings.json or STATUS=OK HOOK_FILE=.claude/settings.local.json/hooks to reload configurationchmod +x)set -euo pipefail in bash scripts for safety$CLAUDE_PROJECT_DIR for project-relative pathsTemplate mode (recommended for common tools):
# User: "Set up ESLint to run on file edits"
/create-hook
# Detects "ESLint", offers template
# → Creates .claude/hooks/run-eslint.sh
# → Adds PostToolUse hook to settings.json
# → Makes script executable
# → STATUS=OK HOOK_FILE=.claude/hooks/run-eslint.sh
Interactive mode:
/create-hook
# Shows menu of:
# 1. Common tools (ESLint, Prettier, TypeScript, Tests)
# 2. Hook types (PreToolUse, PostToolUse, etc.)
# 3. Custom hook creation
Guided mode:
/create-hook PostToolUse
# Guides through creating a PostToolUse hook
# Asks for: matcher, command/script, storage location
Direct mode:
/create-hook PreToolUse "Bash" "set -euo pipefail; cmd=\$(jq -r '.tool_input.command'); echo \"[\$(date -Iseconds)] \$cmd\" >> \"\$CLAUDE_USER_DIR/bash.log\""
# Creates hook configuration directly with best practices
For complete documentation, see: https://docs.claude.com/en/hooks
/create-hookGuided hook creation with brainstorming and security-first design. Triggers: new hook, create hook, hook creation, start hook, build hook, add hook, write hook, PreToolUse, PostToolUse, UserPromptSubmit Use when: creating a new hook from scratch, need security-first design guidance, want structured workflow for hook development with Socratic questioning DO NOT use when: evaluating existing hooks - use /hooks-eval instead. DO NOT use when: deciding where to place hooks - use hook-scope-guide skill. DO NOT use when: validating hook security - use /validate-hook instead. Use this command to create any new hook. Security validation is automatic.