Hook Dev Skill
This skill helps create production-ready Claude Code hooks following Anthropic's official specifications.
What are Hooks?
Hooks are user-defined shell commands that execute at various points in Claude Code's lifecycle. They provide:
- Deterministic control: Ensure actions happen automatically, not relying on LLM decisions
- Workflow automation: Format code, run linters, log commands
- Custom notifications: Alert when Claude needs input
- Permission systems: Block sensitive file modifications
- Feedback loops: Validate code changes against conventions
Hook vs Skill vs Command
| Feature | Hook | Skill | Command |
|---|---|---|---|
| Trigger | Lifecycle event | Context match | User invocation |
| Type | Shell command | AI capability | Prompt template |
| Use case | Automation, validation | Autonomous features | Reusable prompts |
| Control | Deterministic | LLM-driven | User-initiated |
When to use Hooks: Automatic formatting, logging, notifications, permission control, code validation
Hook Events
Ten lifecycle events trigger hooks:
1. PreToolUse
Executes after Claude creates tool parameters but before processing.
Use cases:
- Block edits to sensitive files (.env, production configs)
- Validate bash commands before execution
- Request confirmation for destructive operations
- Log all tool usage for auditing
- Modify tool inputs programmatically
Blocking capability: Yes - exit code 2 blocks with error fed to Claude; JSON output with "permissionDecision": "deny" also blocks
2. PostToolUse
Runs immediately after successful tool completion.
Use cases:
- Format code after edits (Prettier, Black, gofmt)
- Run linters after file modifications
- Sync changes to external systems
- Update indexes or caches
Blocking capability: No - tool already executed
3. UserPromptSubmit
Fires when users submit prompts, before Claude processes them.
Use cases:
- Inject context automatically (git status, env variables)
- Log user interactions
- Validate prompt safety
- Add project-specific context
Blocking capability: Yes - JSON output with "decision": "block" prevents submission
4. PermissionRequest
Activates when permission dialogs appear to users.
Use cases:
- Auto-approve safe operations
- Auto-deny dangerous operations
- Log permission requests
- Provide context for decisions
Blocking capability: Yes - can allow, deny, or pass through to user
5. Notification
Triggers when Claude Code sends notifications.
Use cases:
- Custom notification sounds
- Desktop notifications (macOS, Linux, Windows)
- Send to Slack/Discord
- Visual alerts
Blocking capability: No - notification already generated
6. Stop
Activates when the main agent finishes responding.
Use cases:
- Run tests after code generation
- Update documentation
- Commit changes automatically
- Trigger deployments
Blocking capability: Yes - JSON output with "decision": "block" prevents stop
7. SubagentStop
Runs when subagent tasks complete.
Use cases:
- Log subagent results
- Validate subagent outputs
- Trigger next workflow steps
- Aggregate subagent data
Blocking capability: Yes - JSON output with "decision": "block" prevents stop
8. PreCompact
Executes before context window compaction operations.
Use cases:
- Save conversation state
- Export conversation history
- Archive important context
Blocking capability: Yes - can prevent compaction
9. SessionStart
Triggers at session initialization or resumption.
Use cases:
- Load project context
- Initialize environment variables
- Display project status
- Check for updates
Blocking capability: No - session already started
10. SessionEnd
Runs when sessions terminate.
Use cases:
- Save session state
- Cleanup temporary files
- Export logs
- Trigger cleanup scripts
Blocking capability: No - session already ending
Hook Configuration
Hooks are configured in .claude/settings.json:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Edit",
"hooks": [
{
"type": "command",
"command": "echo 'Editing file' >> /tmp/claude-audit.log"
}
]
}
],
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "jq -r '.tool_input.file_path' | { read file_path; if echo \"$file_path\" | grep -q '\\.py$'; then black \"$file_path\"; fi; }"
}
]
}
]
}
}
Configuration Structure
{
"hooks": {
"EventName": [ // Event type (PreToolUse, PostToolUse, etc.)
{
"matcher": "ToolPattern", // Tool name, regex (pipe-separated), or "*" for all
"hooks": [ // Array of hooks for this matcher
{
"type": "command", // "command" or "prompt"
"command": "shell command", // Shell command to execute
"timeout": 60 // Optional timeout in seconds (default: 60)
}
]
}
]
}
}
Configuration Locations
- User-level:
~/.claude/settings.json(all projects) - Project-level:
.claude/settings.json(this project, team-shared) - Local:
.claude/settings.local.json(uncommitted, project-specific overrides) - Plugin-level:
plugin-name/hooks/hooks.json(bundled with plugin)
Matchers
Tool Matching
Exact match:
{
"matcher": "Edit"
}
Regex pattern (use pipe for OR):
{
"matcher": "Edit|Write|MultiEdit"
}
Wildcard (match all tools):
{
"matcher": "*"
}
Common tool names: Bash, Read, Write, Edit, MultiEdit, Glob, Grep, Task, WebFetch, WebSearch, TodoRead, TodoWrite, NotebookRead, NotebookEdit
MCP tools: Use mcp__<server>__<tool> pattern (e.g., mcp__github__create_issue)
Accessing Tool Data
Hook input arrives via stdin as JSON. Use jq to extract values:
# Access file_path from Edit tool
jq -r '.tool_input.file_path'
# Access bash command
jq -r '.tool_input.command'
# Check if file matches pattern
jq -r '.tool_input.file_path' | { read file_path; if echo "$file_path" | grep -q '\.py$'; then echo "Python file"; fi; }
# Access tool response (PostToolUse only)
jq -r '.tool_response'
Common JSON fields:
.session_id- Current session identifier.transcript_path- Path to conversation transcript.cwd- Current working directory.tool_name- Name of the tool being used.tool_input- Tool parameters (varies by tool).tool_response- Tool output (PostToolUse only).hook_event_name- Name of the event
Environment variables:
$CLAUDE_PROJECT_DIR- Project root absolute path$CLAUDE_ENV_FILE- For SessionStart hooks to persist environment variables$CLAUDE_CODE_REMOTE- "true" if remote execution, empty if local${CLAUDE_PLUGIN_ROOT}- Plugin directory path (plugin hooks only). Use this to reference scripts bundled with your plugin:{ "type": "command", "command": "${CLAUDE_PLUGIN_ROOT}/scripts/process.sh" }
Hook Types
Command Hooks
Execute bash scripts with stdin JSON input:
{
"type": "command",
"command": "jq -r '.tool_input.file_path' | xargs -I {} prettier --write {}"
}
Exit codes:
0- Success; stdout shown to user (except UserPromptSubmit shows to Claude)2- Blocking error; stderr fed to Claude as context- Other - Non-blocking error; stderr shown to user
Prompt Hooks
Send input to LLM (Haiku) for decisions (PreToolUse, Stop, SubagentStop, UserPromptSubmit):
{
"type": "prompt",
"prompt": "Analyze this tool use and decide if it should be allowed: {{input}}"
}
Returns JSON with decision fields.