hook-development
Canonical guide to authoring Claude Code hooks, both prompt-based and command-based. PROACTIVELY activate for: (1) creating a hook, (2) adding PreToolUse or PostToolUse hooks, (3) validating tool use, (4) implementing prompt-based hooks, (5) blocking dangerous commands, (6) event-driven automation, (7) configuring hooks.json, (8) using ${CLAUDE_PLUGIN_ROOT}, (9) hook matchers and filtering, (10) debugging hooks that do not fire. Provides: hooks.json schema, event reference, security guidance, and examples.
How do I install this agent skill?
npx skills add https://github.com/josiahsiegel/claude-plugin-marketplace --skill hook-developmentIs this agent skill safe to install?
- Gen Agent Trust Hubpass
This skill provides educational documentation and templates for developing Claude Code hooks. It does not contain executable code, remote downloads, or malicious patterns.
- Socketpass
No alerts
- Snykpass
Risk: LOW · No issues
- Runlayerpass
1 file scanned · No issues
What does this agent skill do?
Hook Development for Claude Code Plugins
Overview
Hooks are event-driven automation that execute in response to Claude Code events. Use hooks to validate operations, enforce policies, load context, and integrate external tools.
Two hook types:
- Prompt-based (recommended): LLM-driven, context-aware decisions
- Command-based: Shell commands for fast, deterministic checks
Hook Events Reference
| Event | When | Common Use |
|---|---|---|
| PreToolUse | Before tool executes | Validate, approve/deny, modify input |
| PostToolUse | After tool completes | Test, lint, log, provide feedback |
| Stop | Main agent stopping | Verify task completeness |
| SubagentStop | Subagent stopping | Validate subagent work |
| UserPromptSubmit | User sends prompt | Add context, validate, preprocess |
| SessionStart | Session begins | Load context, set environment |
| SessionEnd | Session ends | Cleanup, logging |
| PreCompact | Before context compaction | Preserve critical information |
| Notification | Notification shown | Custom alert reactions |
Configuration Formats
Plugin hooks.json (in hooks/hooks.json)
Uses wrapper format with hooks field:
{
"description": "What these hooks do (optional)",
"hooks": {
"PreToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PLUGIN_ROOT}/scripts/validate.sh",
"timeout": 10
}
]
}
]
}
}
User settings format (in .claude/settings.json)
Direct format, no wrapper:
{
"PreToolUse": [
{
"matcher": "Write|Edit",
"hooks": [{ "type": "command", "command": "script.sh" }]
}
]
}
Critical difference: Plugin hooks.json wraps events inside {"hooks": {...}}. Settings format puts events at top level.
Prompt-Based Hooks (Recommended)
Use LLM reasoning for context-aware decisions:
{
"type": "prompt",
"prompt": "Evaluate if this tool use is appropriate. Check for: system paths, credentials, path traversal. Return 'approve' or 'deny'.",
"timeout": 30
}
Supported events: PreToolUse, PostToolUse, Stop, SubagentStop, UserPromptSubmit
Benefits: Context-aware, flexible, better edge case handling, easier to maintain.
Command Hooks
Execute shell commands for deterministic checks:
{
"type": "command",
"command": "bash ${CLAUDE_PLUGIN_ROOT}/scripts/validate.sh",
"timeout": 60
}
Always use ${CLAUDE_PLUGIN_ROOT} for portable paths.
Exit Codes
| Code | Meaning |
|---|---|
| 0 | Success (stdout shown in transcript) |
| 2 | Blocking error (stderr fed back to Claude) |
| Other | Non-blocking error |
Matchers
Control which tools trigger hooks:
"matcher": "Write" // Exact match
"matcher": "Write|Edit|Bash" // Multiple tools
"matcher": "mcp__.*__delete.*" // Regex (all MCP delete tools)
"matcher": "*" // All tools (use sparingly)
Matchers are case-sensitive.
Hook Input/Output
Input (all hooks receive via stdin)
{
"session_id": "abc123",
"transcript_path": "/path/to/transcript.txt",
"cwd": "/current/working/dir",
"hook_event_name": "PreToolUse",
"tool_name": "Write",
"tool_input": { "file_path": "/path/to/file" }
}
Event-specific fields: tool_name, tool_input, tool_result, user_prompt, reason
Access in prompts: $TOOL_INPUT, $TOOL_RESULT, $USER_PROMPT
Output
Standard (all hooks):
{
"continue": true,
"suppressOutput": false,
"systemMessage": "Message for Claude"
}
PreToolUse decisions:
{
"hookSpecificOutput": {
"permissionDecision": "allow|deny|ask",
"updatedInput": { "field": "modified_value" }
}
}
Stop/SubagentStop decisions:
{
"decision": "approve|block",
"reason": "Explanation"
}
Environment Variables
| Variable | Available | Purpose |
|---|---|---|
$CLAUDE_PLUGIN_ROOT | All hooks | Plugin directory (portable paths) |
$CLAUDE_PROJECT_DIR | All hooks | Project root path |
$CLAUDE_ENV_FILE | SessionStart only | Persist env vars for session |
SessionStart can persist variables:
echo "export PROJECT_TYPE=nodejs" >> "$CLAUDE_ENV_FILE"
Common Patterns
Validate file writes (PreToolUse)
{
"PreToolUse": [{
"matcher": "Write|Edit",
"hooks": [{
"type": "prompt",
"prompt": "Check if this file write is safe. Deny writes to: .env, credentials, system paths, or files with path traversal (..). Return 'approve' or 'deny' with reason."
}]
}]
}
Auto-test after changes (PostToolUse)
{
"PostToolUse": [{
"matcher": "Write|Edit",
"hooks": [{
"type": "command",
"command": "npm test -- --bail",
"timeout": 60
}]
}]
}
Verify task completion (Stop)
{
"Stop": [{
"matcher": "*",
"hooks": [{
"type": "prompt",
"prompt": "Verify: tests run, build succeeded, all questions answered. Return 'approve' to stop or 'block' with reason to continue."
}]
}]
}
Load project context (SessionStart)
{
"SessionStart": [{
"matcher": "*",
"hooks": [{
"type": "command",
"command": "bash ${CLAUDE_PLUGIN_ROOT}/scripts/load-context.sh",
"timeout": 10
}]
}]
}
Security Best Practices
In command hook scripts:
#!/bin/bash
set -euo pipefail
input=$(cat)
file_path=$(echo "$input" | jq -r '.tool_input.file_path')
# Always validate inputs
if [[ ! "$file_path" =~ ^[a-zA-Z0-9_./-]+$ ]]; then
echo '{"decision": "deny", "reason": "Invalid path"}' >&2
exit 2
fi
# Block path traversal
if [[ "$file_path" == *".."* ]]; then
echo '{"decision": "deny", "reason": "Path traversal detected"}' >&2
exit 2
fi
# Block sensitive files
if [[ "$file_path" == *".env"* ]]; then
echo '{"decision": "deny", "reason": "Sensitive file"}' >&2
exit 2
fi
# Always quote variables
echo "$file_path"
Lifecycle and Limitations
Hooks load at session start. Changes to hook configuration require restarting Claude Code.
- Editing
hooks/hooks.jsonwon't affect the current session - Adding new hook scripts won't be recognized until restart
- All matching hooks run in parallel (not sequentially)
- Hooks don't see each other's output - design for independence
To test changes: Exit Claude Code, restart with claude or claude --debug.
Debugging
# Enable debug mode to see hook execution
claude --debug
# Test hook scripts directly
echo '{"tool_name": "Write", "tool_input": {"file_path": "/test"}}' | \
bash ${CLAUDE_PLUGIN_ROOT}/scripts/validate.sh
# Validate hook JSON output
output=$(./hook-script.sh < test-input.json)
echo "$output" | jq .
# View loaded hooks in session
# Use /hooks command
Validation Checklist
- hooks.json uses correct format (plugin wrapper or settings direct)
- All script paths use
${CLAUDE_PLUGIN_ROOT}(no hardcoded paths) - Scripts are executable and handle errors (
set -euo pipefail) - Scripts validate all inputs and quote all variables
- Matchers are specific (avoid
*unless necessary) - Timeouts are set appropriately (default: command 60s, prompt 30s)
- Hook output is valid JSON
- Tested with
claude --debug
How can the creator link this skill?
Add the canonical catalog link to the repository README so users can inspect current installs and available audits. The publishing guide covers the complete discovery path.
<a href="https://skillzs.dev/skills/josiahsiegel/claude-plugin-marketplace/hook-development">View hook-development on skillZs</a>