claude-hook-authoring
This skill should be used when creating hooks, automating workflows, or when "PreToolUse", "PostToolUse", "hooks.json", "event handler", or "create hook" are mentioned.
Packaged view
This page reorganizes the original catalog entry around fit, installability, and workflow context first. The original raw source lives below.
Install command
npx @skill-hub/cli install outfitter-dev-agents-claude-hook-authoring
Repository
Skill path: agent-kit/skills/claude-hook-authoring
This skill should be used when creating hooks, automating workflows, or when "PreToolUse", "PostToolUse", "hooks.json", "event handler", or "create hook" are mentioned.
Open repositoryBest for
Primary workflow: Ship Full Stack.
Technical facets: Full Stack.
Target audience: everyone.
License: Unknown.
Original source
Catalog source: SkillHub Club.
Repository owner: outfitter-dev.
This is still a mirrored public skill entry. Review the repository before installing into production workflows.
What it helps with
- Install claude-hook-authoring into Claude Code, Codex CLI, Gemini CLI, or OpenCode workflows
- Review https://github.com/outfitter-dev/agents before adding claude-hook-authoring to shared team environments
- Use claude-hook-authoring for development workflows
Works across
Favorites: 0.
Sub-skills: 0.
Aggregator: No.
Original source / Raw SKILL.md
---
name: claude-hook-authoring
version: 2.0.0
description: This skill should be used when creating hooks, automating workflows, or when "PreToolUse", "PostToolUse", "hooks.json", "event handler", or "create hook" are mentioned.
user-invocable: true
---
# Claude Hook Authoring
Create event hooks that automate workflows, validate operations, and respond to Claude Code events.
## Hook Types
Two hook execution types:
| Type | Best For | Example |
|------|----------|---------|
| **prompt** | Complex reasoning, context-aware validation | LLM evaluates if action is safe |
| **command** | Deterministic checks, external tools, performance | Bash script validates paths |
**Prompt hooks** (recommended for complex logic):
```json
{
"type": "prompt",
"prompt": "Evaluate if this file write is safe: $TOOL_INPUT. Check for sensitive paths, credentials, path traversal. Return 'allow' or 'deny' with reason.",
"timeout": 30
}
```
**Command hooks** (for deterministic/fast checks):
```json
{
"type": "command",
"command": "${CLAUDE_PLUGIN_ROOT}/scripts/validate.sh",
"timeout": 10
}
```
## Hook Events
| Event | When | Can Block | Common Uses |
|-------|------|-----------|-------------|
| **PreToolUse** | Before tool executes | Yes | Validate commands, check paths, enforce policies |
| **PostToolUse** | After tool succeeds | No | Auto-format, run linters, update docs |
| **PostToolUseFailure** | After tool fails | No | Error logging, retry logic, notifications |
| **PermissionRequest** | Permission dialog shown | Yes | Auto-allow/deny based on rules |
| **UserPromptSubmit** | User submits prompt | No | Add context, log activity, augment prompts |
| **Notification** | Claude sends notification | No | External alerts, logging |
| **Stop** | Main agent finishes | No | Cleanup, completion notifications |
| **SubagentStart** | Subagent spawns | No | Track subagent usage |
| **SubagentStop** | Subagent finishes | No | Log results, trigger follow-ups |
| **PreCompact** | Before context compacts | No | Backup conversation, preserve context |
| **SessionStart** | Session starts/resumes | No | Load context, show status, init resources |
| **SessionEnd** | Session ends | No | Cleanup, save state, log metrics |
See [references/hook-types.md](references/hook-types.md) for detailed documentation of each event.
## Quick Start
### Auto-Format TypeScript
```json
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit(*.ts|*.tsx)",
"hooks": [{
"type": "command",
"command": "biome check --write \"$file\"",
"timeout": 10
}]
}
]
}
}
```
### Block Dangerous Commands
```json
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [{
"type": "command",
"command": "$CLAUDE_PROJECT_DIR/.claude/hooks/validate-bash.sh",
"timeout": 5
}]
}
]
}
}
```
**validate-bash.sh**:
```bash
#!/usr/bin/env bash
set -euo pipefail
INPUT=$(cat)
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty')
if echo "$COMMAND" | grep -qE '\brm\s+-rf\s+/'; then
echo "Dangerous command blocked: rm -rf /" >&2
exit 2 # Exit 2 = block and show error to Claude
fi
exit 0
```
### Smart Validation with Prompt Hook
```json
{
"hooks": {
"PreToolUse": [
{
"matcher": "Write|Edit",
"hooks": [{
"type": "prompt",
"prompt": "Analyze this file operation for safety. Check: 1) No sensitive paths (/etc, ~/.ssh), 2) No credentials in content, 3) No path traversal (..). Tool input: $TOOL_INPUT. Respond with JSON: {\"decision\": \"allow|deny\", \"reason\": \"...\"}",
"timeout": 30
}]
}
]
}
}
```
## Configuration Locations
| Location | Scope | Committed |
|----------|-------|-----------|
| `.claude/settings.json` | Project (team-shared) | Yes |
| `.claude/settings.local.json` | Project (local only) | No |
| `~/.claude/settings.json` | Personal (all projects) | No |
| `plugin/hooks/hooks.json` | Plugin | Yes |
### Plugin Format (hooks.json)
Uses wrapper structure:
```json
{
"description": "Plugin hooks for auto-formatting",
"hooks": {
"PostToolUse": [...]
}
}
```
### Settings Format (settings.json)
Direct structure (no wrapper):
```json
{
"hooks": {
"PostToolUse": [...]
}
}
```
## Matchers
Matchers determine which tool invocations trigger the hook. Case-sensitive.
```json
{"matcher": "Write"} // Exact match
{"matcher": "Edit|Write"} // Multiple tools (OR)
{"matcher": "*"} // All tools
{"matcher": "Write(*.py)"} // File pattern
{"matcher": "Write|Edit(*.ts|*.tsx)"} // Multiple + pattern
{"matcher": "mcp__memory__.*"} // MCP server tools
{"matcher": "mcp__github__create_issue"} // Specific MCP tool
```
**Lifecycle hooks** (SessionStart, SessionEnd, Stop, Notification) use special matchers:
```json
// SessionStart matchers
{"matcher": "startup"} // Initial start
{"matcher": "resume"} // --resume or --continue
{"matcher": "clear"} // After /clear
{"matcher": "compact"} // After compaction
// PreCompact matchers
{"matcher": "manual"} // User triggered /compact
{"matcher": "auto"} // Automatic compaction
```
See [references/matchers.md](references/matchers.md) for advanced patterns.
## Input Format
All hooks receive JSON on stdin:
```json
{
"session_id": "abc123",
"transcript_path": "/path/to/transcript.jsonl",
"cwd": "/current/working/directory",
"hook_event_name": "PreToolUse",
"permission_mode": "ask",
"tool_name": "Write",
"tool_input": {
"file_path": "/project/src/file.ts",
"content": "export const foo = 'bar';"
}
}
```
**Event-specific fields**:
- Tool hooks: `tool_name`, `tool_input`, `tool_result` (PostToolUse)
- UserPromptSubmit: `user_prompt`
- Stop/SubagentStop: `reason`
**Prompt hooks** access fields via: `$TOOL_INPUT`, `$TOOL_RESULT`, `$USER_PROMPT`
### Reading Input
**Bash**:
```bash
#!/usr/bin/env bash
set -euo pipefail
INPUT=$(cat)
TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name')
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
```
**Bun/TypeScript**:
```typescript
#!/usr/bin/env bun
const input = await Bun.stdin.json();
const toolName = input.tool_name;
const filePath = input.tool_input?.file_path;
```
## Output Format
### Exit Codes (Simple)
```bash
exit 0 # Success, continue execution
exit 2 # Block operation (PreToolUse only), stderr shown to Claude
exit 1 # Warning, stderr shown to user, continues
```
### JSON Output (Advanced)
```json
{
"continue": true,
"suppressOutput": false,
"systemMessage": "Context for Claude",
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "allow|deny|ask",
"permissionDecisionReason": "Explanation",
"updatedInput": {"modified": "field"}
}
}
```
**PreToolUse** can modify tool input via `updatedInput` and control permissions via `permissionDecision`.
## Environment Variables
| Variable | Availability | Description |
|----------|--------------|-------------|
| `$CLAUDE_PROJECT_DIR` | All hooks | Project root directory |
| `$CLAUDE_PLUGIN_ROOT` | Plugin hooks | Plugin root (use for portable paths) |
| `$file` | PostToolUse (Write/Edit) | Path to affected file |
| `$CLAUDE_ENV_FILE` | SessionStart | Write env vars here to persist |
| `$CLAUDE_CODE_REMOTE` | All hooks | Set if running in remote context |
**Plugin hooks** should always use `${CLAUDE_PLUGIN_ROOT}` for portability:
```json
{
"command": "${CLAUDE_PLUGIN_ROOT}/scripts/validate.sh"
}
```
**SessionStart** can persist environment variables:
```bash
#!/usr/bin/env bash
# Persist variables for the session
echo "export PROJECT_TYPE=nodejs" >> "$CLAUDE_ENV_FILE"
echo "export API_URL=https://api.example.com" >> "$CLAUDE_ENV_FILE"
```
## Component-Scoped Hooks
Skills, agents, and commands can define hooks in frontmatter. These hooks only run when the component is active.
**Supported events**: PreToolUse, PostToolUse, Stop
### Skill with Hooks
```yaml
---
name: my-skill
description: Skill with validation hooks
hooks:
PreToolUse:
- matcher: "Write|Edit"
hooks:
- type: prompt
prompt: "Validate this write operation for the skill context..."
---
```
### Agent with Hooks
```yaml
---
name: security-reviewer
model: sonnet
hooks:
PreToolUse:
- matcher: "Bash"
hooks:
- type: command
command: "${CLAUDE_PLUGIN_ROOT}/scripts/validate-bash.sh"
Stop:
- matcher: "*"
hooks:
- type: prompt
prompt: "Verify the security review is complete..."
---
```
## Execution Model
**Parallel execution**: All matching hooks run in parallel, not sequentially.
```json
{
"PreToolUse": [{
"matcher": "Write",
"hooks": [
{"type": "command", "command": "check1.sh"}, // Runs in parallel
{"type": "command", "command": "check2.sh"}, // Runs in parallel
{"type": "prompt", "prompt": "Validate..."} // Runs in parallel
]
}]
}
```
**Implications**:
- Hooks cannot see each other's output
- Non-deterministic ordering
- Design for independence
**Hot-swap limitations**: Hook changes require restarting Claude Code. Editing `hooks.json` or hook scripts does not affect the current session.
## Security Best Practices
1. **Validate all input** - Check for path traversal, sensitive paths, injection
2. **Quote shell variables** - Always use `"$VAR"` not `$VAR`
3. **Set timeouts** - Prevent hanging hooks (default: 60s command, 30s prompt)
4. **Use absolute paths** - Via `$CLAUDE_PROJECT_DIR` or `${CLAUDE_PLUGIN_ROOT}`
5. **Handle errors gracefully** - Use `set -euo pipefail` in bash
6. **Don't log sensitive data** - Filter credentials, tokens, API keys
See [references/security.md](references/security.md) for detailed security patterns.
## Debugging
```bash
# Run Claude with debug output
claude --debug
# Test hook manually
echo '{"tool_name": "Write", "tool_input": {"file_path": "test.ts"}}' | ./.claude/hooks/my-hook.sh
# Check transcript for hook execution
# Press Ctrl+R in Claude Code to view transcript
```
**Common issues**:
- Hook not firing: Check matcher syntax, restart Claude Code
- Permission errors: `chmod +x script.sh`
- Timeout: Increase timeout value or optimize script
## Workflow Patterns
### Pre-Commit Quality Gate
```json
{
"hooks": {
"PreToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{"type": "command", "command": "./.claude/hooks/validate-paths.sh"},
{"type": "command", "command": "./.claude/hooks/check-sensitive.sh"}
]
}
],
"PostToolUse": [
{
"matcher": "Write|Edit(*.ts)",
"hooks": [
{"type": "command", "command": "biome check --write \"$file\""},
{"type": "command", "command": "tsc --noEmit \"$file\""}
]
}
]
}
}
```
### Context Injection
```json
{
"hooks": {
"SessionStart": [{
"matcher": "startup",
"hooks": [{
"type": "command",
"command": "echo \"Branch: $(git branch --show-current)\" && git status --short"
}]
}],
"UserPromptSubmit": [{
"matcher": "*",
"hooks": [{
"type": "command",
"command": "echo \"Time: $(date '+%Y-%m-%d %H:%M %Z')\""
}]
}]
}
}
```
## References
- [references/hook-types.md](references/hook-types.md) - Detailed documentation for each hook event
- [references/matchers.md](references/matchers.md) - Advanced matcher patterns and MCP tools
- [references/security.md](references/security.md) - Security best practices and validation patterns
- [references/schema.md](references/schema.md) - Complete configuration schema reference
- [references/examples.md](references/examples.md) - Real-world hook implementations
## External Resources
- [Official Hooks Reference](https://code.claude.com/docs/en/hooks)
- [Hooks Guide](https://code.claude.com/docs/en/hooks-guide)
- [Community Examples (disler)](https://github.com/disler/claude-code-hooks-mastery)
- [Claude Code Showcase](https://github.com/ChrisWiles/claude-code-showcase)
---
## Referenced Files
> The following files are referenced in this skill and included for context.
### references/hook-types.md
```markdown
# Hook Types Reference
Detailed documentation for each Claude Code hook event.
## Tool Hooks
### PreToolUse
Executes **before** a tool runs. Can block or modify tool execution.
**Timing**: After Claude creates tool parameters, before tool executes
**Can block**: Yes (exit code 2 or `permissionDecision: "deny"`)
**Supports**: Both `command` and `prompt` hook types
**Input fields**:
- `tool_name`: Name of the tool being called
- `tool_input`: Parameters being passed to the tool
**Output capabilities**:
- Block execution with exit code 2 or `permissionDecision: "deny"`
- Modify input with `updatedInput` in JSON response
- Ask user with `permissionDecision: "ask"`
- Provide context via `systemMessage`
**Common matchers**:
```json
"Bash" // Shell commands
"Write" // File writing
"Edit" // File editing
"Read" // File reading
"Write|Edit" // Multiple tools
"Write(*.py)" // File patterns
"mcp__memory__.*" // MCP tools
"*" // All tools
```
**Use cases**:
- Validate bash commands before execution
- Check file paths for security issues
- Block dangerous operations
- Add context before execution
- Enforce security policies
- Log tool invocations
- Modify tool input on the fly
**Example - Block dangerous commands**:
```json
{
"PreToolUse": [{
"matcher": "Bash",
"hooks": [{
"type": "command",
"command": "./.claude/hooks/validate-bash.sh",
"timeout": 5
}]
}]
}
```
**Example - Smart validation with prompt**:
```json
{
"PreToolUse": [{
"matcher": "Write|Edit",
"hooks": [{
"type": "prompt",
"prompt": "Analyze this file operation. Check for: 1) sensitive paths, 2) credentials in content, 3) path traversal. Tool: $TOOL_INPUT. Return {\"decision\": \"allow|deny\", \"reason\": \"...\"}",
"timeout": 30
}]
}]
}
```
**Example - Modify tool input**:
```bash
#!/usr/bin/env bash
# Add timestamp to all file writes
INPUT=$(cat)
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path')
CONTENT=$(echo "$INPUT" | jq -r '.tool_input.content')
# Add header to content
NEW_CONTENT="// Modified $(date -Iseconds)\n$CONTENT"
cat << EOF
{
"continue": true,
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "allow",
"updatedInput": {
"file_path": "$FILE_PATH",
"content": "$NEW_CONTENT"
}
}
}
EOF
```
### PostToolUse
Executes **after** a tool completes successfully.
**Timing**: Immediately after tool returns success
**Can block**: No
**Supports**: `command` hook type only
**Input fields**:
- `tool_name`: Name of the tool that ran
- `tool_input`: Parameters that were passed
- `tool_result`: Result returned by the tool
**Special variables**:
- `$file`: Path to affected file (Write/Edit tools only)
**Common matchers**:
```json
"Write|Edit(*.ts)" // TypeScript files
"Write(*.py)" // Python files
"Write|Edit" // Any file modification
"*" // All successful tools
```
**Use cases**:
- Auto-format code files
- Run linters
- Update documentation
- Trigger builds
- Send notifications
- Update indexes
**Example - Auto-format TypeScript**:
```json
{
"PostToolUse": [{
"matcher": "Write|Edit(*.ts|*.tsx)",
"hooks": [{
"type": "command",
"command": "biome check --write \"$file\"",
"timeout": 10
}]
}]
}
```
**Example - Chain multiple formatters**:
```json
{
"PostToolUse": [{
"matcher": "Write|Edit(*.py)",
"hooks": [
{"type": "command", "command": "black \"$file\"", "timeout": 10},
{"type": "command", "command": "isort \"$file\"", "timeout": 5},
{"type": "command", "command": "mypy \"$file\"", "timeout": 15}
]
}]
}
```
### PostToolUseFailure
Executes **after** a tool fails.
**Timing**: After tool execution fails
**Can block**: No
**Supports**: `command` hook type
**Input fields**:
- `tool_name`: Name of the tool that failed
- `tool_input`: Parameters that were passed
- `error`: Error information
**Use cases**:
- Error logging and analytics
- Retry logic
- Failure notifications
- Error recovery
- Debug information collection
**Example - Log failures**:
```json
{
"PostToolUseFailure": [{
"matcher": "*",
"hooks": [{
"type": "command",
"command": "./.claude/hooks/log-failure.sh",
"timeout": 5
}]
}]
}
```
### PermissionRequest
Executes when a permission dialog would be shown to the user.
**Timing**: Before showing permission dialog
**Can block**: Yes (via `permissionDecision`)
**Supports**: Both `command` and `prompt` hook types
**Input fields**:
- `tool_name`: Tool requesting permission
- `tool_input`: Parameters being requested
**Output capabilities**:
- Auto-allow with `permissionDecision: "allow"`
- Auto-deny with `permissionDecision: "deny"`
- Show dialog with `permissionDecision: "ask"` (default)
**Use cases**:
- Auto-approve known-safe operations
- Auto-deny high-risk operations
- Implement custom permission policies
- Reduce permission fatigue for trusted patterns
**Example - Auto-approve safe reads**:
```json
{
"PermissionRequest": [{
"matcher": "Read",
"hooks": [{
"type": "command",
"command": "./.claude/hooks/auto-approve-reads.sh",
"timeout": 3
}]
}]
}
```
## User Interaction Hooks
### UserPromptSubmit
Executes when user submits a prompt to Claude.
**Timing**: After user submits, before Claude processes
**Can block**: No
**Supports**: Both `command` and `prompt` hook types
**Input fields**:
- `user_prompt`: The prompt text submitted
**Matcher**: Always `*`
**Use cases**:
- Add timestamp or date context
- Add environment information
- Log user activity
- Pre-process or augment prompts
- Add project context
- Skill matching and suggestion
**Example - Add timestamp**:
```json
{
"UserPromptSubmit": [{
"matcher": "*",
"hooks": [{
"type": "command",
"command": "echo \"Current time: $(date '+%Y-%m-%d %H:%M:%S %Z')\"",
"timeout": 2
}]
}]
}
```
**Example - Add git context**:
```json
{
"UserPromptSubmit": [{
"matcher": "*",
"hooks": [{
"type": "command",
"command": "echo \"Branch: $(git branch --show-current 2>/dev/null || echo 'N/A')\"",
"timeout": 3
}]
}]
}
```
### Notification
Executes when Claude Code sends a notification.
**Timing**: When notification is triggered
**Can block**: No
**Supports**: `command` hook type
**Input fields**:
- Notification message and metadata
**Matcher**: Always `*`
**Use cases**:
- Send to external systems (Slack, email)
- Log notifications
- Trigger alerts
- Update dashboards
- Archive important messages
- Text-to-speech announcements
**Example - Slack integration**:
```json
{
"Notification": [{
"matcher": "*",
"hooks": [{
"type": "command",
"command": "./.claude/hooks/send-to-slack.sh",
"timeout": 10
}]
}]
}
```
## Agent Lifecycle Hooks
### Stop
Executes when main Claude agent finishes responding.
**Timing**: After Claude completes response
**Can block**: No
**Supports**: Both `command` and `prompt` hook types
**Input fields**:
- `reason`: Why the agent stopped
**Matcher**: Always `*`
**Use cases**:
- Clean up temporary resources
- Send completion notifications
- Update external systems
- Log session metrics
- Archive conversation
- Verify task completion
**Example - Completion notification**:
```json
{
"Stop": [{
"matcher": "*",
"hooks": [{
"type": "command",
"command": "echo 'Task completed at $(date +%H:%M)'",
"timeout": 2
}]
}]
}
```
**Example - Verify completeness with prompt**:
```json
{
"Stop": [{
"matcher": "*",
"hooks": [{
"type": "prompt",
"prompt": "Review if the task was completed satisfactorily. Check for any unfinished work or follow-up items.",
"timeout": 30
}]
}]
}
```
### SubagentStart
Executes when a subagent (Task tool) spawns.
**Timing**: When subagent is created
**Can block**: No
**Supports**: `command` hook type
**Input fields**:
- Subagent metadata
**Matcher**: Always `*`
**Use cases**:
- Track subagent spawning
- Log subagent parameters
- Monitor parallel execution
- Resource allocation
**Example - Track subagent usage**:
```json
{
"SubagentStart": [{
"matcher": "*",
"hooks": [{
"type": "command",
"command": "./.claude/hooks/log-subagent-start.sh",
"timeout": 2
}]
}]
}
```
### SubagentStop
Executes when a subagent (Task tool) finishes.
**Timing**: After subagent completes
**Can block**: No
**Supports**: Both `command` and `prompt` hook types
**Input fields**:
- `reason`: Why the subagent stopped
- Subagent result metadata
**Matcher**: Always `*`
**Use cases**:
- Track subagent completion
- Log subagent results
- Trigger follow-up actions
- Update metrics
- Debug subagent behavior
**Example - Log subagent completion**:
```json
{
"SubagentStop": [{
"matcher": "*",
"hooks": [{
"type": "command",
"command": "./.claude/hooks/log-subagent-stop.sh",
"timeout": 3
}]
}]
}
```
## Session Lifecycle Hooks
### SessionStart
Executes when session starts or resumes.
**Timing**: At session initialization
**Can block**: No
**Supports**: `command` hook type
**Input fields**:
- `reason`: Start type
**Matchers**:
```json
"startup" // Claude Code starts fresh
"resume" // Session resumes (--resume or --continue)
"clear" // After /clear command
"compact" // After compaction
```
**Special capability**: Persist environment variables via `$CLAUDE_ENV_FILE`
**Use cases**:
- Display welcome message
- Show git status
- Load project context
- Check for updates
- Initialize resources
- Set session-wide variables
**Example - Welcome with git status**:
```json
{
"SessionStart": [{
"matcher": "startup",
"hooks": [{
"type": "command",
"command": "echo 'Welcome!' && git status --short",
"timeout": 5
}]
}]
}
```
**Example - Persist environment variables**:
```bash
#!/usr/bin/env bash
# This script runs on SessionStart
# Persist variables for the entire session
# Detect project type and persist
if [[ -f "package.json" ]]; then
echo "export PROJECT_TYPE=nodejs" >> "$CLAUDE_ENV_FILE"
elif [[ -f "Cargo.toml" ]]; then
echo "export PROJECT_TYPE=rust" >> "$CLAUDE_ENV_FILE"
fi
# Set API endpoints
echo "export API_URL=https://api.example.com" >> "$CLAUDE_ENV_FILE"
```
### SessionEnd
Executes when session ends.
**Timing**: Before session terminates
**Can block**: No
**Supports**: `command` hook type
**Input fields**:
- `reason`: End type
**Matchers** (reasons):
```json
"clear" // User ran /clear
"logout" // User logged out
"prompt_input_exit" // Exited during prompt input
"other" // Other reasons
```
**Use cases**:
- Clean up resources
- Save state
- Log session metrics
- Send completion notifications
- Archive transcripts
**Example - Cleanup**:
```json
{
"SessionEnd": [{
"matcher": "*",
"hooks": [{
"type": "command",
"command": "./.claude/hooks/cleanup.sh",
"timeout": 5
}]
}]
}
```
### PreCompact
Executes before conversation compacts.
**Timing**: Before compact operation starts
**Can block**: No
**Supports**: `command` hook type
**Input fields**:
- Compact trigger type
**Matchers**:
```json
"manual" // User triggered via /compact
"auto" // Automatic compact (context limit)
```
**Use cases**:
- Backup conversation
- Archive important context
- Update external summaries
- Log compact events
- Prepare for context reset
**Example - Backup before compact**:
```json
{
"PreCompact": [{
"matcher": "manual|auto",
"hooks": [{
"type": "command",
"command": "./.claude/hooks/backup-conversation.sh",
"timeout": 10
}]
}]
}
```
## Hook Type Comparison
| Event | Can Block | Prompt Type | Command Type | Common Use |
|-------|-----------|-------------|--------------|------------|
| PreToolUse | Yes | Yes | Yes | Validation, security |
| PostToolUse | No | No | Yes | Formatting, linting |
| PostToolUseFailure | No | No | Yes | Error logging |
| PermissionRequest | Yes | Yes | Yes | Auto-approve/deny |
| UserPromptSubmit | No | Yes | Yes | Context injection |
| Notification | No | No | Yes | External alerts |
| Stop | No | Yes | Yes | Cleanup, verification |
| SubagentStart | No | No | Yes | Tracking |
| SubagentStop | No | Yes | Yes | Logging |
| SessionStart | No | No | Yes | Initialization |
| SessionEnd | No | No | Yes | Cleanup |
| PreCompact | No | No | Yes | Backup |
## Tool Use ID Correlation
PreToolUse and PostToolUse events for the same tool invocation share a tool use ID, allowing you to correlate them:
```bash
#!/usr/bin/env bash
# PreToolUse - save state
INPUT=$(cat)
TOOL_USE_ID=$(echo "$INPUT" | jq -r '.tool_use_id')
TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name')
# Save start time for correlation
echo "$(date +%s%N)" > "/tmp/claude-tool-$TOOL_USE_ID.start"
```
```bash
#!/usr/bin/env bash
# PostToolUse - calculate duration
INPUT=$(cat)
TOOL_USE_ID=$(echo "$INPUT" | jq -r '.tool_use_id')
START=$(cat "/tmp/claude-tool-$TOOL_USE_ID.start" 2>/dev/null || echo "0")
END=$(date +%s%N)
DURATION_MS=$(( (END - START) / 1000000 ))
echo "Tool completed in ${DURATION_MS}ms"
rm -f "/tmp/claude-tool-$TOOL_USE_ID.start"
```
```
### references/matchers.md
```markdown
# Matcher Patterns Reference
Matchers determine which tool invocations or events trigger a hook. They are case-sensitive strings that support exact matching, regex patterns, wildcards, and file patterns.
## Matcher Types
### Simple String Match
Match exact tool name:
```json
{"matcher": "Write"} // Only Write tool
{"matcher": "Edit"} // Only Edit tool
{"matcher": "Bash"} // Only Bash tool
{"matcher": "Read"} // Only Read tool
{"matcher": "Grep"} // Only Grep tool
{"matcher": "Glob"} // Only Glob tool
{"matcher": "Task"} // Only Task tool (subagents)
{"matcher": "WebFetch"} // Only WebFetch tool
{"matcher": "WebSearch"}// Only WebSearch tool
```
### OR Patterns (Pipe)
Match multiple tools with `|`:
```json
{"matcher": "Edit|Write"} // Edit OR Write
{"matcher": "Read|Grep|Glob"} // Any read/search operation
{"matcher": "Write|Edit|NotebookEdit"} // Multiple specific tools
{"matcher": "WebFetch|WebSearch"} // Web operations
```
### Wildcard Match
Match all tools with `*`:
```json
{"matcher": "*"} // Matches everything
```
**Use cases**:
- Logging all tool usage
- Global validation
- Universal context injection
- Metrics collection
### File Pattern Match
Match tools operating on specific file types with `(pattern)`:
```json
{"matcher": "Write(*.py)"} // Write Python files
{"matcher": "Edit(*.ts)"} // Edit TypeScript files
{"matcher": "Write(*.md)"} // Write Markdown files
{"matcher": "Write|Edit(*.js)"} // Write or Edit JavaScript
{"matcher": "Write|Edit(*.ts|*.tsx)"} // TypeScript and TSX files
```
**Supported patterns**:
- `*.ext` - Any file with extension
- `path/*.ext` - Files in specific directory (relative to project)
- `**/*.ext` - Recursive file match
**More examples**:
```json
{"matcher": "Write(*.tsx)"} // React components
{"matcher": "Write|Edit(*.rs)"} // Rust files
{"matcher": "Write(src/**/*.ts)"} // TS files in src/
{"matcher": "Edit(.env*)"} // .env files
{"matcher": "Write(*.json)"} // JSON files
{"matcher": "Write|Edit(*.yaml|*.yml)"} // YAML files
```
### Regex Patterns
Full regex support for complex matching:
```json
{"matcher": "^Write$"} // Exactly "Write", no prefix/suffix
{"matcher": ".*Edit.*"} // Contains "Edit" anywhere
{"matcher": "Notebook.*"} // Starts with "Notebook"
{"matcher": "Bash|WebFetch"} // Bash or WebFetch
```
**Regex features**:
- `|` - OR operator
- `.` - Any character
- `*` - Zero or more
- `+` - One or more
- `^` - Start of string
- `$` - End of string
- `[abc]` - Character class
- `\w` - Word character
- `\d` - Digit
## MCP Tool Matchers
MCP (Model Context Protocol) tools follow naming: `mcp__<server-name>__<tool-name>`
### Match All MCP Tools
```json
{"matcher": "mcp__.*__.*"} // Any MCP tool from any server
```
### Match Specific Server
```json
{"matcher": "mcp__memory__.*"} // All memory MCP tools
{"matcher": "mcp__github__.*"} // All GitHub MCP tools
{"matcher": "mcp__filesystem__.*"} // All filesystem MCP tools
{"matcher": "mcp__brave-search__.*"}// All Brave search tools
```
### Match Specific Tools
```json
{"matcher": "mcp__github__create_issue"} // Specific GitHub tool
{"matcher": "mcp__github__create_pull_request"} // Create PR tool
{"matcher": "mcp__memory__add_memory"} // Add to memory
{"matcher": "mcp__memory__search_memory"} // Search memory
```
### Complex MCP Patterns
```json
// All delete operations across MCP servers
{"matcher": "mcp__.*__delete.*"}
// Create operations in GitHub
{"matcher": "mcp__github__(create_issue|create_comment|create_pull_request)"}
// All read operations in filesystem
{"matcher": "mcp__filesystem__(read|list|search).*"}
// Dangerous operations to block
{"matcher": "mcp__.*(delete|remove|destroy).*"}
```
## Lifecycle Event Matchers
Some hooks use special matchers for lifecycle events instead of tool names.
### SessionStart Matchers
```json
{"matcher": "startup"} // Fresh Claude Code start
{"matcher": "resume"} // Session resume (--resume, --continue)
{"matcher": "clear"} // After /clear command
{"matcher": "compact"} // After context compaction
{"matcher": "*"} // Any session start type
```
### SessionEnd Matchers
```json
{"matcher": "clear"} // User ran /clear
{"matcher": "logout"} // User logged out
{"matcher": "prompt_input_exit"} // Exited during prompt
{"matcher": "other"} // Other reasons
{"matcher": "*"} // Any end reason
```
### PreCompact Matchers
```json
{"matcher": "manual"} // User triggered /compact
{"matcher": "auto"} // Automatic compaction
{"matcher": "*"} // Any compact type
```
### Stop/SubagentStop Matchers
```json
{"matcher": "*"} // Always matches (lifecycle events)
```
## Complex Matcher Examples
### Multiple Tools with File Patterns
```json
// Format Python or TypeScript
{"matcher": "Write|Edit(*.py)|Write|Edit(*.ts)"}
// All code files
{"matcher": "Write|Edit(*.ts|*.tsx|*.js|*.jsx|*.py|*.rs)"}
```
### Excluding Patterns
There's no direct exclusion, but you can handle this in the hook script:
```bash
#!/usr/bin/env bash
INPUT=$(cat)
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
# Skip test files
if [[ "$FILE_PATH" =~ \.(test|spec)\. ]]; then
exit 0
fi
# Skip node_modules
if [[ "$FILE_PATH" =~ node_modules/ ]]; then
exit 0
fi
# Continue with validation...
```
### Combining with Regex
```json
// Bash or any MCP tool
{"matcher": "Bash|mcp__.*__.*"}
// Read operations (multiple tools)
{"matcher": "Read|Grep|Glob|WebFetch"}
// File modifications only
{"matcher": "Write|Edit|NotebookEdit"}
```
## Matcher Debugging
If your hook isn't firing, check:
1. **Case sensitivity**: `Write` works, `write` doesn't
2. **Exact tool names**: Use `claude --debug` to see actual tool names
3. **File patterns**: Ensure the pattern matches the file path format
4. **MCP naming**: Verify server and tool names match exactly
### Testing Matchers
```bash
# See what tools Claude is calling
claude --debug 2>&1 | grep "tool_name"
# Test regex patterns
echo "Write" | grep -E '^Write$' # Should match
echo "WriteFile" | grep -E '^Write$' # Should not match
```
## Common Matcher Patterns
### Security Validation
```json
// All file operations
{"matcher": "Write|Edit|Read"}
// Dangerous commands
{"matcher": "Bash"}
// Network operations
{"matcher": "WebFetch|WebSearch|mcp__.*"}
```
### Auto-Formatting
```json
// TypeScript/JavaScript
{"matcher": "Write|Edit(*.ts|*.tsx|*.js|*.jsx)"}
// Python
{"matcher": "Write|Edit(*.py)"}
// Rust
{"matcher": "Write|Edit(*.rs)"}
// All supported
{"matcher": "Write|Edit(*.ts|*.tsx|*.py|*.rs|*.go)"}
```
### Logging
```json
// All tool operations
{"matcher": "*"}
// All MCP operations
{"matcher": "mcp__.*__.*"}
// File operations only
{"matcher": "Write|Edit|Read|Grep|Glob"}
```
### External Integration
```json
// GitHub operations
{"matcher": "mcp__github__.*"}
// Memory operations
{"matcher": "mcp__memory__.*"}
// All external services
{"matcher": "mcp__.*__.*|WebFetch|WebSearch"}
```
```
### references/security.md
```markdown
# Security Best Practices
Comprehensive security guidance for Claude Code hooks.
## Input Validation
### Validate All Input
Always validate and sanitize hook input before use:
```bash
#!/usr/bin/env bash
set -euo pipefail
INPUT=$(cat)
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
# Validate input exists
if [[ -z "$FILE_PATH" ]]; then
echo "Error: file_path missing" >&2
exit 1
fi
# Validate format
if [[ ! "$FILE_PATH" =~ ^[a-zA-Z0-9_./-]+$ ]]; then
echo "Error: invalid characters in file path" >&2
exit 2
fi
```
### Check for Path Traversal
Block directory traversal attacks:
```bash
#!/usr/bin/env bash
set -euo pipefail
INPUT=$(cat)
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
# Block path traversal
if echo "$FILE_PATH" | grep -qE '\.\./'; then
cat << EOF >&2
Path traversal detected: $FILE_PATH
Paths containing '..' are not allowed.
EOF
exit 2
fi
# Block absolute paths outside project
if [[ "$FILE_PATH" == /* ]] && [[ ! "$FILE_PATH" == "$CLAUDE_PROJECT_DIR"* ]]; then
echo "Access outside project directory blocked: $FILE_PATH" >&2
exit 2
fi
```
### Block Sensitive System Paths
Prevent access to system files:
```bash
#!/usr/bin/env bash
set -euo pipefail
INPUT=$(cat)
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
# Blocked system paths
BLOCKED_PATHS=(
'^/etc/'
'^/root/'
'^/home/[^/]+/\.ssh/'
'^/var/log/'
'^/sys/'
'^/proc/'
'^/boot/'
'^/usr/bin/'
'^/usr/sbin/'
)
for pattern in "${BLOCKED_PATHS[@]}"; do
if echo "$FILE_PATH" | grep -qE "$pattern"; then
cat << EOF >&2
Access to sensitive system path blocked: $FILE_PATH
This path is restricted for security reasons.
EOF
exit 2
fi
done
```
### Detect Sensitive Files
Warn or block access to sensitive project files:
```bash
#!/usr/bin/env bash
set -euo pipefail
INPUT=$(cat)
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
# Sensitive file patterns
SENSITIVE_PATTERNS=(
'\.env$'
'\.env\.'
'id_rsa'
'id_ed25519'
'\.pem$'
'\.key$'
'\.p12$'
'credentials'
'password'
'token'
'secret'
'\.git/config$'
'\.npmrc$'
'\.pypirc$'
)
for pattern in "${SENSITIVE_PATTERNS[@]}"; do
if echo "$FILE_PATH" | grep -qiE "$pattern"; then
cat << EOF >&2
Warning: Accessing sensitive file: $FILE_PATH
This file may contain sensitive information.
EOF
# Could exit 2 to block, or continue with warning
fi
done
```
## Command Injection Prevention
### Always Quote Variables
```bash
# WRONG - vulnerable to injection
rm $FILE_PATH
cd $DIRECTORY
echo $CONTENT
# CORRECT - properly quoted
rm "$FILE_PATH"
cd "$DIRECTORY"
echo "$CONTENT"
```
### Avoid eval
```bash
# WRONG - dangerous
eval "$USER_COMMAND"
# CORRECT - use specific commands
if [[ "$USER_COMMAND" == "format" ]]; then
black "$FILE_PATH"
fi
```
### Validate Command Patterns
```bash
#!/usr/bin/env bash
set -euo pipefail
INPUT=$(cat)
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty')
# Block dangerous command patterns
DANGEROUS_PATTERNS=(
'\brm\s+-rf\s+/' # rm -rf /
'\brm\s+--no-preserve-root' # rm --no-preserve-root
'\bmkfs\b' # filesystem format
'\bdd\s+if=' # disk destruction
'\bformat\s+[cC]:' # Windows format
'>\s*/dev/sd[a-z]' # overwrite disk
':()\{\s*:\|\:&\s*\};:' # Fork bomb
'\bchmod\s+777\s+/' # Dangerous permissions
'\bchown\s+.*\s+/' # System ownership change
'\bcurl\s+.*\|\s*bash' # Pipe to bash
'\bwget\s+.*\|\s*bash' # Pipe to bash
'\bsudo\s+rm' # Sudo rm
'\bgit\s+push\s+--force\s+origin\s+main' # Force push main
)
for pattern in "${DANGEROUS_PATTERNS[@]}"; do
if echo "$COMMAND" | grep -qE "$pattern"; then
cat << EOF >&2
Dangerous command blocked: $COMMAND
Pattern matched: $pattern
EOF
exit 2
fi
done
```
## Path Security
### Use Absolute Paths
Always construct paths from known roots:
```bash
#!/usr/bin/env bash
# Use CLAUDE_PROJECT_DIR for project paths
SCRIPT_PATH="$CLAUDE_PROJECT_DIR/.claude/hooks/helper.sh"
# Use CLAUDE_PLUGIN_ROOT for plugin paths
PLUGIN_SCRIPT="${CLAUDE_PLUGIN_ROOT}/scripts/validate.sh"
# Never rely on relative paths
# BAD: ./scripts/validate.sh
# GOOD: "$CLAUDE_PROJECT_DIR/.claude/scripts/validate.sh"
```
### Validate Script Existence
```bash
#!/usr/bin/env bash
SCRIPT_PATH="$CLAUDE_PROJECT_DIR/.claude/hooks/helper.sh"
# Check exists
if [[ ! -f "$SCRIPT_PATH" ]]; then
echo "Error: script not found: $SCRIPT_PATH" >&2
exit 1
fi
# Check executable
if [[ ! -x "$SCRIPT_PATH" ]]; then
echo "Error: script not executable: $SCRIPT_PATH" >&2
exit 1
fi
# Execute safely
"$SCRIPT_PATH" "$@"
```
### Resolve Symlinks
```bash
#!/usr/bin/env bash
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path')
# Resolve symlinks to check actual destination
REAL_PATH=$(realpath "$FILE_PATH" 2>/dev/null || echo "$FILE_PATH")
# Check the real path is within project
if [[ ! "$REAL_PATH" == "$CLAUDE_PROJECT_DIR"* ]]; then
echo "Symlink points outside project: $FILE_PATH -> $REAL_PATH" >&2
exit 2
fi
```
## Sensitive Data Protection
### Never Log Sensitive Data
```bash
#!/usr/bin/env bash
INPUT=$(cat)
# WRONG - logs everything including secrets
echo "Input: $INPUT" >> /tmp/debug.log
# CORRECT - log only safe fields
TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name')
echo "Tool: $TOOL_NAME" >> /tmp/debug.log
# CORRECT - filter sensitive fields before logging
echo "$INPUT" | jq 'del(.tool_input.password, .tool_input.api_key, .tool_input.token)' >> /tmp/debug.log
```
### Sanitize Output
```bash
#!/usr/bin/env bash
INPUT=$(cat)
CONTENT=$(echo "$INPUT" | jq -r '.tool_input.content // empty')
# Check for secrets in content
if echo "$CONTENT" | grep -qiE '(password|api_key|secret|token)\s*[=:]\s*\S+'; then
echo "Warning: Potential secret detected in content" >&2
# Could block or just warn
fi
```
### Protect Environment Variables
```bash
#!/usr/bin/env bash
# Don't expose sensitive env vars
# WRONG
echo "API_KEY=$API_KEY"
env | grep -i secret
# CORRECT - never print secrets
echo "API key configured: $([ -n "$API_KEY" ] && echo "yes" || echo "no")"
```
## Timeout Protection
### Set Appropriate Timeouts
```json
{
"hooks": {
"PreToolUse": [{
"matcher": "Bash",
"hooks": [{
"type": "command",
"command": "./.claude/hooks/validate.sh",
"timeout": 5
}]
}],
"PostToolUse": [{
"matcher": "Write|Edit",
"hooks": [{
"type": "command",
"command": "./.claude/hooks/format.sh",
"timeout": 30
}]
}]
}
}
```
**Guidelines**:
- Quick validation: 3-5 seconds
- Formatting: 10-30 seconds
- Network operations: 30-60 seconds
- Default: 60 seconds for command, 30 seconds for prompt
### Handle Timeouts Gracefully
```bash
#!/usr/bin/env bash
set -euo pipefail
# Set internal timeout for network operations
timeout 10 curl -s https://api.example.com/validate || {
echo "API validation skipped (timeout)" >&2
exit 0 # Don't block on timeout
}
```
## Error Handling
### Use Strict Mode
```bash
#!/usr/bin/env bash
set -euo pipefail # Exit on error, undefined vars, pipe failures
# Also consider:
set -E # Inherit ERR trap in functions
trap 'echo "Error on line $LINENO" >&2' ERR
```
### Validate Dependencies
```bash
#!/usr/bin/env bash
set -euo pipefail
# Check required tools exist
for cmd in jq git curl; do
if ! command -v "$cmd" &>/dev/null; then
echo "Error: $cmd not installed" >&2
exit 1
fi
done
```
### Handle JSON Parsing Errors
```bash
#!/usr/bin/env bash
set -euo pipefail
# Read input with error handling
INPUT=$(cat) || {
echo "Error: failed to read stdin" >&2
exit 1
}
# Parse with validation
if ! echo "$INPUT" | jq empty 2>/dev/null; then
echo "Error: invalid JSON input" >&2
exit 1
fi
TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // empty')
if [[ -z "$TOOL_NAME" ]]; then
echo "Error: tool_name missing" >&2
exit 1
fi
```
## Permission Control
### PreToolUse Permission Decisions
```bash
#!/usr/bin/env bash
set -euo pipefail
INPUT=$(cat)
TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name')
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
# Auto-approve reads of non-sensitive files
if [[ "$TOOL_NAME" == "Read" ]] && [[ ! "$FILE_PATH" =~ \.(env|key|pem)$ ]]; then
cat << EOF
{
"continue": true,
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "allow"
}
}
EOF
exit 0
fi
# Ask for writes to core files
if [[ "$FILE_PATH" =~ src/core/ ]]; then
cat << EOF
{
"continue": true,
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "ask",
"permissionDecisionReason": "Write to core module requires confirmation"
}
}
EOF
exit 0
fi
# Default: allow
exit 0
```
### PermissionRequest Hook
```bash
#!/usr/bin/env bash
set -euo pipefail
INPUT=$(cat)
TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name')
# Auto-deny certain operations
if [[ "$TOOL_NAME" =~ (delete|destroy|remove) ]]; then
cat << EOF
{
"hookSpecificOutput": {
"permissionDecision": "deny",
"permissionDecisionReason": "Destructive operations require manual approval"
}
}
EOF
exit 0
fi
```
## Audit Trail
### Log All Operations
```bash
#!/usr/bin/env bash
set -euo pipefail
AUDIT_FILE="$CLAUDE_PROJECT_DIR/.claude/audit.log"
INPUT=$(cat)
TIMESTAMP=$(date -Iseconds)
HOOK_EVENT=$(echo "$INPUT" | jq -r '.hook_event_name')
TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // "N/A"')
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // "N/A"')
SESSION_ID=$(echo "$INPUT" | jq -r '.session_id')
# Create audit entry (filter sensitive data)
AUDIT_ENTRY=$(jq -n \
--arg ts "$TIMESTAMP" \
--arg event "$HOOK_EVENT" \
--arg tool "$TOOL_NAME" \
--arg file "$FILE_PATH" \
--arg session "$SESSION_ID" \
--arg user "$USER" \
'{
timestamp: $ts,
event: $event,
tool: $tool,
file: $file,
session: $session,
user: $user
}')
echo "$AUDIT_ENTRY" >> "$AUDIT_FILE"
# Rotate: keep only last 10000 entries
tail -n 10000 "$AUDIT_FILE" > "$AUDIT_FILE.tmp" && mv "$AUDIT_FILE.tmp" "$AUDIT_FILE"
exit 0
```
## Security Checklist
### Before Deploying Hooks
- [ ] All input validated and sanitized
- [ ] Path traversal attacks blocked
- [ ] Sensitive system paths protected
- [ ] All shell variables quoted
- [ ] No eval or command injection vectors
- [ ] Sensitive data not logged
- [ ] Appropriate timeouts set
- [ ] Dependencies validated
- [ ] Error handling robust
- [ ] Audit trail enabled
### Regular Security Review
- [ ] Review hook scripts for vulnerabilities
- [ ] Check for hardcoded secrets
- [ ] Verify timeout values are appropriate
- [ ] Audit logged data for sensitive info leaks
- [ ] Update blocked patterns for new threats
- [ ] Test hooks with malicious input
```
### references/schema.md
```markdown
# Hook Reference
Comprehensive technical reference for Claude Code event hooks.
## Table of Contents
1. [Hook Configuration Schema](#hook-configuration-schema)
2. [Hook Events](#hook-events)
3. [Matcher Patterns](#matcher-patterns)
4. [Input Format](#input-format)
5. [Output Format](#output-format)
6. [Environment Variables](#environment-variables)
7. [Exit Codes](#exit-codes)
8. [Hook Chaining](#hook-chaining)
9. [Security Best Practices](#security-best-practices)
10. [MCP Integration](#mcp-integration)
11. [Plugin Hooks](#plugin-hooks)
12. [Advanced Patterns](#advanced-patterns)
## Hook Configuration Schema
### Location
Hooks are configured in JSON settings files:
| Location | Scope | Committed |
|----------|-------|-----------|
| `~/.claude/settings.json` | Personal (all projects) | No |
| `.claude/settings.json` | Project (shared with team) | Yes |
| `.claude/settings.local.json` | Project (local overrides) | No |
| `plugin/hooks/hooks.json` | Plugin | Yes |
### Basic Structure
```json
{
"hooks": {
"<EventName>": [
{
"matcher": "<ToolPattern>",
"hooks": [
{
"type": "command",
"command": "<shell-command>",
"timeout": 30
}
]
}
]
}
}
```
### Field Reference
#### `hooks` (root)
**Type**: Object
**Required**: Yes
**Description**: Root object containing all hook definitions
```json
{
"hooks": {
// Event configurations here
}
}
```
#### Event Name Keys
**Type**: String (key)
**Required**: At least one
**Valid values**:
- `PreToolUse`
- `PostToolUse`
- `UserPromptSubmit`
- `Notification`
- `Stop`
- `SubagentStop`
- `PreCompact`
- `SessionStart`
- `SessionEnd`
**Description**: Event type that triggers the hook
```json
{
"hooks": {
"PreToolUse": [...],
"PostToolUse": [...],
"SessionStart": [...]
}
}
```
#### Event Configuration Array
**Type**: Array of objects
**Description**: Array of matcher/hooks pairs for an event
```json
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write(*.py)",
"hooks": [...]
},
{
"matcher": "Edit(*.ts)",
"hooks": [...]
}
]
}
}
```
#### `matcher`
**Type**: String
**Required**: Yes
**Description**: Pattern to match tools or event types
**Syntax options:**
- Simple: `"Write"` - Exact tool name
- Regex: `"Edit|Write"` - OR pattern
- Wildcard: `"*"` - All tools
- File pattern: `"Write(*.py)"` - File extension
- MCP: `"mcp__server__tool"` - MCP tool pattern
```json
{"matcher": "Write|Edit"}
```
#### `hooks` (nested)
**Type**: Array of objects
**Required**: Yes
**Description**: Commands to execute when matcher triggers
```json
{
"hooks": [
{
"type": "command",
"command": "black \"$file\"",
"timeout": 30
}
]
}
```
#### `type`
**Type**: String
**Required**: Yes
**Valid values**: `"command"`
**Description**: Hook execution type (currently only "command" supported)
#### `command`
**Type**: String
**Required**: Yes
**Description**: Shell command to execute
**Features:**
- Variable expansion: `$file`, `$CLAUDE_PROJECT_DIR`
- Stdin: Receives JSON input
- Stdout: Shown to user
- Stderr: Error messages
- Exit code: Controls behavior
```json
{
"type": "command",
"command": "./.claude/hooks/format-code.sh"
}
```
#### `timeout`
**Type**: Number (seconds)
**Required**: No
**Default**: 30
**Description**: Maximum execution time
```json
{
"type": "command",
"command": "./slow-operation.sh",
"timeout": 60
}
```
### Complete Example
```json
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "$CLAUDE_PROJECT_DIR/.claude/hooks/validate-bash.sh",
"timeout": 5
}
]
},
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "$CLAUDE_PROJECT_DIR/.claude/hooks/check-paths.sh",
"timeout": 3
}
]
}
],
"PostToolUse": [
{
"matcher": "Write(*.ts)",
"hooks": [
{
"type": "command",
"command": "biome check --write \"$file\"",
"timeout": 10
}
]
},
{
"matcher": "Write(*.py)",
"hooks": [
{
"type": "command",
"command": "black \"$file\"",
"timeout": 10
}
]
}
],
"SessionStart": [
{
"matcher": "startup",
"hooks": [
{
"type": "command",
"command": "echo 'Session started' && git status",
"timeout": 5
}
]
}
]
}
}
```
## Hook Events
### PreToolUse
Executes **before** a tool runs. Can block or modify execution.
**Timing**: After Claude creates tool parameters, before tool execution
**Input**: Tool name and full input parameters
**Can block**: Yes (exit code 2)
**Common matchers**:
- `Bash` - Shell commands
- `Write` - File writing
- `Edit` - File editing
- `Read` - File reading
- `Grep` - Content search
- `Glob` - File patterns
- `WebFetch` - Web operations
- `WebSearch` - Web search
- `Task` - Subagent tasks
- `*` - All tools
**Use cases**:
- Validate bash commands before execution
- Check file paths for security issues
- Block dangerous operations
- Add context before execution
- Enforce security policies
- Log tool invocations
**Example**:
```json
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [{
"type": "command",
"command": "./.claude/hooks/validate-bash.sh",
"timeout": 5
}]
}
]
}
}
```
### PostToolUse
Executes **after** a tool completes successfully.
**Timing**: Immediately after tool returns success
**Input**: Tool name, input parameters, and execution result
**Can block**: No (but can report issues)
**Common matchers**:
- `Write(*.ext)` - Specific file types
- `Edit(*.ext)` - Specific file types
- `Write|Edit` - Any file modification
- `*` - All successful tools
**Use cases**:
- Auto-format code files
- Run linters
- Update documentation
- Trigger builds
- Send notifications
- Update indexes
**Example**:
```json
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit(*.ts)",
"hooks": [{
"type": "command",
"command": "biome check --write \"$file\"",
"timeout": 10
}]
}
]
}
}
```
### UserPromptSubmit
Executes when user submits a prompt to Claude.
**Timing**: After user submits, before Claude processes
**Input**: User prompt text and session metadata
**Can block**: No
**Matcher**: Always `*`
**Use cases**:
- Add timestamp or date context
- Add environment information
- Log user activity
- Pre-process or augment prompts
- Add project context
**Example**:
```json
{
"hooks": {
"UserPromptSubmit": [
{
"matcher": "*",
"hooks": [{
"type": "command",
"command": "./.claude/hooks/add-context.sh",
"timeout": 2
}]
}
]
}
}
```
### Notification
Executes when Claude Code sends a notification.
**Timing**: When notification is triggered
**Input**: Notification message and metadata
**Can block**: No
**Matcher**: Always `*`
**Use cases**:
- Send to external systems (Slack, email)
- Log notifications
- Trigger alerts
- Update dashboards
- Archive important messages
**Example**:
```json
{
"hooks": {
"Notification": [
{
"matcher": "*",
"hooks": [{
"type": "command",
"command": "./.claude/hooks/send-to-slack.sh",
"timeout": 5
}]
}
]
}
}
```
### Stop
Executes when main Claude agent finishes responding.
**Timing**: After Claude completes response
**Input**: Session metadata and completion reason
**Can block**: No
**Matcher**: Always `*`
**Use cases**:
- Clean up temporary resources
- Send completion notifications
- Update external systems
- Log session metrics
- Archive conversation
**Example**:
```json
{
"hooks": {
"Stop": [
{
"matcher": "*",
"hooks": [{
"type": "command",
"command": "./.claude/hooks/on-completion.sh",
"timeout": 5
}]
}
]
}
}
```
### SubagentStop
Executes when a subagent (Task tool) finishes.
**Timing**: After subagent completes
**Input**: Subagent metadata and result
**Can block**: No
**Matcher**: Always `*`
**Use cases**:
- Track subagent usage
- Log subagent results
- Trigger follow-up actions
- Update metrics
- Debug subagent behavior
**Example**:
```json
{
"hooks": {
"SubagentStop": [
{
"matcher": "*",
"hooks": [{
"type": "command",
"command": "./.claude/hooks/log-subagent.sh",
"timeout": 3
}]
}
]
}
}
```
### PreCompact
Executes before conversation compacts.
**Timing**: Before compact operation starts
**Input**: Compact trigger type
**Can block**: No
**Matchers**:
- `manual` - User triggered via `/compact`
- `auto` - Automatic compact
**Use cases**:
- Backup conversation
- Archive important context
- Update external summaries
- Log compact events
- Prepare for reset
**Example**:
```json
{
"hooks": {
"PreCompact": [
{
"matcher": "manual",
"hooks": [{
"type": "command",
"command": "./.claude/hooks/backup-conversation.sh",
"timeout": 10
}]
}
]
}
}
```
### SessionStart
Executes when session starts or resumes.
**Timing**: At session initialization
**Input**: Session start reason
**Can block**: No
**Matchers**:
- `startup` - Claude Code starts
- `resume` - Session resumes (`--resume`, `--continue`)
- `clear` - After `/clear` command
- `compact` - After compact operation
**Use cases**:
- Display welcome message
- Show git status
- Load project context
- Check for updates
- Initialize resources
**Example**:
```json
{
"hooks": {
"SessionStart": [
{
"matcher": "startup",
"hooks": [{
"type": "command",
"command": "echo 'Welcome!' && git status",
"timeout": 5
}]
}
]
}
}
```
### SessionEnd
Executes when session ends.
**Timing**: Before session terminates
**Input**: End reason
**Can block**: No
**Matchers** (reasons):
- `clear` - User ran `/clear`
- `logout` - User logged out
- `prompt_input_exit` - Exited during prompt input
- `other` - Other reasons
**Use cases**:
- Clean up resources
- Save state
- Log session metrics
- Send completion notifications
- Archive transcripts
**Example**:
```json
{
"hooks": {
"SessionEnd": [
{
"matcher": "*",
"hooks": [{
"type": "command",
"command": "./.claude/hooks/cleanup.sh",
"timeout": 5
}]
}
]
}
}
```
## Matcher Patterns
### Simple String Match
Match exact tool name:
```json
{"matcher": "Write"} // Only Write tool
{"matcher": "Edit"} // Only Edit tool
{"matcher": "Bash"} // Only Bash tool
{"matcher": "Read"} // Only Read tool
```
### Regex Patterns
Use `|` for OR logic:
```json
{"matcher": "Edit|Write"} // Edit OR Write
{"matcher": "Read|Grep|Glob"} // Any read operation
{"matcher": "Notebook.*"} // Any Notebook tool
{"matcher": "Write|Edit|NotebookEdit"} // Multiple tools
```
**Regex features:**
- `|` - OR operator
- `.` - Any character
- `*` - Zero or more
- `+` - One or more
- `^` - Start of string
- `$` - End of string
**Examples:**
```json
{"matcher": "^Write$"} // Exactly "Write", no prefix/suffix
{"matcher": ".*Edit.*"} // Contains "Edit" anywhere
{"matcher": "Bash|WebFetch"} // Bash or WebFetch
```
### Wildcard Match
Match all tools:
```json
{"matcher": "*"} // Matches everything
```
**Use cases:**
- Logging all tool usage
- Global validation
- Universal context injection
- Metrics collection
### File Pattern Match
Match tools with specific file patterns:
```json
{"matcher": "Write(*.py)"} // Write Python files
{"matcher": "Edit(*.ts)"} // Edit TypeScript files
{"matcher": "Write(*.md)"} // Write Markdown files
{"matcher": "Write|Edit(*.js)"} // Write or Edit JavaScript
```
**Supported patterns:**
- `*.ext` - Any file with extension
- `path/*.ext` - Files in specific directory
- `**/*.ext` - Recursive file match
**Examples:**
```json
{"matcher": "Write(*.tsx)"} // React components
{"matcher": "Write|Edit(*.rs)"} // Rust files
{"matcher": "Write(src/**/*.ts)"} // TS files in src/
{"matcher": "Edit(.env*)"} // .env files
```
### MCP Tool Match
Match MCP server tools:
```json
{"matcher": "mcp__memory__.*"} // Any memory MCP tool
{"matcher": "mcp__github__.*"} // Any GitHub MCP tool
{"matcher": "mcp__.*__.*"} // Any MCP tool
{"matcher": "mcp__linear__create_issue"} // Specific MCP tool
```
**MCP tool naming**: `mcp__<server-name>__<tool-name>`
**Examples:**
```json
// Match all memory operations
{"matcher": "mcp__memory__.*"}
// Match specific GitHub operations
{"matcher": "mcp__github__(create_issue|create_comment)"}
// Match all MCP tools
{"matcher": "mcp__.*__.*"}
// Match Linear issue creation
{"matcher": "mcp__linear__create_issue"}
```
### Complex Matchers
Combine patterns with regex:
```json
// Format Python or TypeScript files
{"matcher": "Write|Edit(*.py)|Write|Edit(*.ts)"}
// Format code files, exclude tests
{"matcher": "Write|Edit(*.ts|*.py)"}
// Bash or any MCP tool
{"matcher": "Bash|mcp__.*__.*"}
// Read operations (multiple tools)
{"matcher": "Read|Grep|Glob|WebFetch"}
```
## Input Format
### JSON Schema
Hooks receive JSON on stdin:
```typescript
interface HookInput {
session_id: string;
transcript_path: string;
cwd: string;
hook_event_name: string;
tool_name?: string;
tool_input?: Record<string, any>;
reason?: string;
[key: string]: any;
}
```
### Common Fields
#### All Events
```json
{
"session_id": "abc123-def456-ghi789",
"transcript_path": "/path/to/transcript.jsonl",
"cwd": "/current/working/directory",
"hook_event_name": "PreToolUse"
}
```
#### Tool Events (PreToolUse, PostToolUse)
```json
{
"session_id": "abc123",
"transcript_path": "/path/to/transcript.jsonl",
"cwd": "/project/root",
"hook_event_name": "PreToolUse",
"tool_name": "Write",
"tool_input": {
"file_path": "/project/root/src/file.ts",
"content": "export const foo = 'bar';"
}
}
```
#### Session Events
```json
{
"session_id": "abc123",
"transcript_path": "/path/to/transcript.jsonl",
"cwd": "/project/root",
"hook_event_name": "SessionStart",
"reason": "startup"
}
```
### Tool-Specific Input
#### Bash Tool
```json
{
"tool_name": "Bash",
"tool_input": {
"command": "git status",
"description": "Check git status"
}
}
```
#### Write Tool
```json
{
"tool_name": "Write",
"tool_input": {
"file_path": "/absolute/path/to/file.ts",
"content": "file contents here"
}
}
```
#### Edit Tool
```json
{
"tool_name": "Edit",
"tool_input": {
"file_path": "/absolute/path/to/file.ts",
"old_string": "const foo = 'old';",
"new_string": "const foo = 'new';",
"replace_all": false
}
}
```
#### Read Tool
```json
{
"tool_name": "Read",
"tool_input": {
"file_path": "/absolute/path/to/file.ts",
"offset": 0,
"limit": 2000
}
}
```
### Reading Input
#### Bash
```bash
#!/usr/bin/env bash
set -euo pipefail
# Read entire input
INPUT=$(cat)
# Parse with jq
TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // empty')
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty')
# Check if field exists
if [[ -z "$TOOL_NAME" ]]; then
echo "Error: tool_name not found" >&2
exit 1
fi
```
#### Bun/TypeScript
```typescript
#!/usr/bin/env bun
import { stdin } from "process";
interface HookInput {
session_id: string;
tool_name?: string;
tool_input?: Record<string, any>;
hook_event_name: string;
}
// Read stdin
const chunks: Buffer[] = [];
for await (const chunk of stdin) {
chunks.push(chunk);
}
const input: HookInput = JSON.parse(Buffer.concat(chunks).toString());
// Access fields
const toolName = input.tool_name;
const filePath = input.tool_input?.file_path;
// Validate
if (!toolName) {
console.error("Error: tool_name missing");
process.exit(1);
}
```
#### Python
```python
#!/usr/bin/env python3
import json
import sys
# Read input
try:
input_data = json.load(sys.stdin)
except json.JSONDecodeError as e:
print(f"Error parsing JSON: {e}", file=sys.stderr)
sys.exit(1)
# Access fields
tool_name = input_data.get("tool_name", "")
file_path = input_data.get("tool_input", {}).get("file_path", "")
# Validate
if not tool_name:
print("Error: tool_name missing", file=sys.stderr)
sys.exit(1)
```
## Output Format
### Exit Codes (Simple)
Most common approach:
```bash
#!/usr/bin/env bash
# Success - continue execution
echo "Validation passed"
exit 0
# Blocking error - show to Claude
echo "Error: dangerous operation detected" >&2
exit 2
# Non-blocking error - show to user
echo "Warning: minor issue detected" >&2
exit 1
```
**Behavior:**
| Exit Code | Behavior | Stdout | Stderr |
|-----------|----------|--------|--------|
| 0 | Success | Shown to user | Ignored |
| 2 | Block (PreToolUse only) | Ignored | Shown to Claude |
| 1 or other | Non-blocking error | Ignored | Shown to user |
### JSON Output (Advanced)
For complex responses:
```json
{
"continue": true,
"stopReason": "Optional stop message",
"suppressOutput": false,
"systemMessage": "Warning or info message",
"decision": "block",
"reason": "Explanation for decision",
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"permissionDecisionReason": "Dangerous operation",
"additionalContext": "Context for Claude"
}
}
```
#### Field Reference
**`continue`** (boolean)
- `true`: Continue execution
- `false`: Stop execution
**`stopReason`** (string)
- Message explaining why stopped
- Shown to user
**`suppressOutput`** (boolean)
- `true`: Hide stdout from user
- `false`: Show stdout
**`systemMessage`** (string)
- Info/warning message
- Shown to user
**`decision`** (string)
- `"block"`: Block operation (PreToolUse)
- `"approve"`: Approve operation
- `undefined`: No decision
**`reason`** (string)
- Explanation for decision
- Shown in context
**`hookSpecificOutput`** (object)
- Event-specific data
- See below for details
#### PreToolUse JSON Output
```json
{
"continue": false,
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"permissionDecisionReason": "Path traversal detected in file path",
"additionalContext": "The file path contains '..' which could allow directory traversal"
}
}
```
**Permission decisions:**
- `"allow"`: Approve tool use
- `"deny"`: Block tool use
- `"ask"`: Ask user for permission
#### Example: Bash with JSON Output
```bash
#!/usr/bin/env bash
set -euo pipefail
INPUT=$(cat)
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
# Check for path traversal
if echo "$FILE_PATH" | grep -q '\.\.'; then
# Output JSON response
cat << EOF
{
"continue": false,
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"permissionDecisionReason": "Path traversal detected",
"additionalContext": "File path contains '..' which is not allowed"
}
}
EOF
exit 0
fi
# Approve
echo "Path validation passed"
exit 0
```
## Environment Variables
> **See also:** [Environment Variables Reference](../../shared/rules/ENV-VARS.md) for comprehensive documentation on `${CLAUDE_PLUGIN_ROOT}` vs `$CLAUDE_PROJECT_DIR`.
### Available Variables
#### `$CLAUDE_PROJECT_DIR`
**Type**: String
**Availability**: All hooks
**Description**: Absolute path to project root directory
```bash
"$CLAUDE_PROJECT_DIR/.claude/hooks/format.sh"
```
**Use cases:**
- Reference project scripts
- Construct relative paths
- Check project structure
#### `$file`
**Type**: String
**Availability**: PostToolUse hooks for Write/Edit tools
**Description**: Absolute path to affected file
```bash
"biome check --write \"$file\""
```
**Use cases:**
- Auto-format files
- Run linters
- Update related files
#### `${CLAUDE_PLUGIN_ROOT}`
**Type**: String
**Availability**: Plugin hooks only
**Description**: Absolute path to plugin root directory
```json
{
"command": "${CLAUDE_PLUGIN_ROOT}/scripts/process.sh"
}
```
**Use cases:**
- Reference plugin scripts
- Load plugin resources
- Access plugin data
### Custom Variables
Define in settings.json:
```json
{
"env": {
"CUSTOM_VAR": "value",
"API_KEY": "secret"
},
"hooks": {
"PostToolUse": [...]
}
}
```
Access in hooks:
```bash
#!/usr/bin/env bash
echo "Custom var: $CUSTOM_VAR"
```
## Exit Codes
### Standard Exit Codes
```bash
0 - Success, continue
1 - Non-blocking error
2 - Blocking error (PreToolUse only)
3+ - Non-blocking error
```
### Exit Code Behavior
#### Exit 0 (Success)
```bash
#!/usr/bin/env bash
echo "Validation passed"
exit 0
```
**Behavior:**
- Execution continues
- Stdout shown to user
- Stderr ignored
**Use for:**
- Successful validation
- Informational output
- Non-critical messages
#### Exit 1 (Warning)
```bash
#!/usr/bin/env bash
echo "Warning: potential issue detected" >&2
exit 1
```
**Behavior:**
- Execution continues
- Stderr shown to user
- Stdout ignored
**Use for:**
- Warnings
- Non-critical issues
- Suggestions
#### Exit 2 (Block)
```bash
#!/usr/bin/env bash
echo "Error: dangerous operation blocked" >&2
exit 2
```
**Behavior:**
- PreToolUse: Blocks tool execution
- PostToolUse: Reports error (doesn't block)
- Stderr shown to Claude
- Stdout ignored
**Use for:**
- Security violations
- Policy enforcement
- Dangerous operations
### Error Handling
Always handle errors gracefully:
```bash
#!/usr/bin/env bash
set -euo pipefail
# Check dependencies
if ! command -v jq &>/dev/null; then
echo "Error: jq not installed" >&2
exit 1
fi
# Validate input
INPUT=$(cat) || {
echo "Error: failed to read stdin" >&2
exit 1
}
# Parse with error handling
TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // empty') || {
echo "Error: failed to parse JSON" >&2
exit 1
}
# Validate required fields
if [[ -z "$TOOL_NAME" ]]; then
echo "Error: tool_name missing" >&2
exit 1
fi
```
## Hook Chaining
### Multiple Hooks per Event
Execute multiple hooks sequentially:
```json
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write(*.ts)",
"hooks": [
{
"type": "command",
"command": "biome check --write \"$file\"",
"timeout": 10
},
{
"type": "command",
"command": "tsc --noEmit \"$file\"",
"timeout": 15
},
{
"type": "command",
"command": "./.claude/hooks/update-index.sh",
"timeout": 5
}
]
}
]
}
}
```
**Execution:**
- Runs in order
- If one fails (non-zero exit), subsequent hooks still run
- All output collected and shown
### Cross-Event Coordination
Use shared state for coordination:
```bash
# PreToolUse: Record operation
#!/usr/bin/env bash
INPUT=$(cat)
TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name')
echo "$TOOL_NAME $(date +%s)" >> /tmp/claude-operations.log
exit 0
```
```bash
# PostToolUse: Update metrics
#!/usr/bin/env bash
OPERATIONS=$(wc -l < /tmp/claude-operations.log)
echo "Total operations: $OPERATIONS" >&2
exit 0
```
## Security Best Practices
### 1. Input Validation
Always validate and sanitize inputs:
```bash
#!/usr/bin/env bash
set -euo pipefail
INPUT=$(cat)
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
# Check for path traversal
if echo "$FILE_PATH" | grep -qE '\.\.|^/etc/|^/root/|^/home/[^/]+/\.ssh/'; then
echo "❌ Dangerous path detected: $FILE_PATH" >&2
exit 2
fi
# Check for sensitive files
if echo "$FILE_PATH" | grep -qE '\.env$|\.git/config|id_rsa|credentials'; then
echo "❌ Sensitive file access blocked: $FILE_PATH" >&2
exit 2
fi
# Validate file extension
if [[ "$FILE_PATH" =~ \.(exe|sh|bin)$ ]]; then
echo "⚠ Warning: executable file" >&2
fi
```
### 2. Command Injection Prevention
Always quote variables:
```bash
# ❌ WRONG - vulnerable to injection
rm $FILE_PATH
# ✅ CORRECT - properly quoted
rm "$FILE_PATH"
# ❌ WRONG - vulnerable
eval "$COMMAND"
# ✅ CORRECT - use array or avoid eval
bash -c "$COMMAND"
```
### 3. Path Security
Use absolute paths and validate:
```bash
#!/usr/bin/env bash
# Get absolute path
SCRIPT_PATH="$CLAUDE_PROJECT_DIR/.claude/hooks/helper.sh"
# Validate script exists
if [[ ! -f "$SCRIPT_PATH" ]]; then
echo "Error: script not found: $SCRIPT_PATH" >&2
exit 1
fi
# Validate script is executable
if [[ ! -x "$SCRIPT_PATH" ]]; then
echo "Error: script not executable: $SCRIPT_PATH" >&2
exit 1
fi
# Execute safely
"$SCRIPT_PATH" "$@"
```
### 4. Sensitive Data Protection
Never log or expose sensitive data:
```bash
#!/usr/bin/env bash
INPUT=$(cat)
# ❌ WRONG - logs sensitive data
echo "Input: $INPUT" >> /tmp/debug.log
# ✅ CORRECT - log only safe fields
TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name')
echo "Tool: $TOOL_NAME" >> /tmp/debug.log
# ✅ Filter sensitive fields
echo "$INPUT" | jq 'del(.tool_input.password, .tool_input.api_key)' >> /tmp/debug.log
```
### 5. Timeout Protection
Set appropriate timeouts:
```json
{
"hooks": {
"PreToolUse": [
{
"hooks": [{
"type": "command",
"command": "./.claude/hooks/validate.sh",
"timeout": 5
}]
}
],
"PostToolUse": [
{
"hooks": [{
"type": "command",
"command": "./.claude/hooks/format.sh",
"timeout": 30
}]
}
]
}
}
```
**Guidelines:**
- Validation: 3-5 seconds
- Formatting: 10-30 seconds
- Network operations: 30-60 seconds
- Heavy operations: Consider running async
### 6. Error Recovery
Handle failures gracefully:
```bash
#!/usr/bin/env bash
set -euo pipefail
# Trap errors
trap 'echo "Error on line $LINENO" >&2' ERR
# Validate dependencies
for cmd in jq git; do
if ! command -v "$cmd" &>/dev/null; then
echo "Error: $cmd not installed" >&2
exit 1
fi
done
# Main logic with error handling
if ! INPUT=$(cat 2>&1); then
echo "Error: failed to read stdin" >&2
exit 1
fi
# Parse with validation
if ! TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name' 2>&1); then
echo "Error: invalid JSON input" >&2
exit 1
fi
```
## MCP Integration
### Matching MCP Tools
MCP tools follow pattern: `mcp__<server>__<tool>`
```json
{
"hooks": {
"PreToolUse": [
{
"matcher": "mcp__memory__.*",
"hooks": [{
"type": "command",
"command": "./.claude/hooks/log-memory-ops.sh"
}]
},
{
"matcher": "mcp__github__create_issue",
"hooks": [{
"type": "command",
"command": "./.claude/hooks/validate-issue.sh"
}]
}
]
}
}
```
### Common MCP Servers
```json
// Memory operations
{"matcher": "mcp__memory__.*"}
// GitHub operations
{"matcher": "mcp__github__.*"}
// Linear operations
{"matcher": "mcp__linear__.*"}
// Filesystem operations
{"matcher": "mcp__filesystem__.*"}
// All MCP tools
{"matcher": "mcp__.*__.*"}
```
### MCP Hook Example
```bash
#!/usr/bin/env bash
# Log MCP operations
set -euo pipefail
INPUT=$(cat)
TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name')
SERVER=$(echo "$TOOL_NAME" | cut -d'_' -f3)
OPERATION=$(echo "$TOOL_NAME" | cut -d'_' -f4-)
echo "[$(date -Iseconds)] MCP $SERVER: $OPERATION" >> "$CLAUDE_PROJECT_DIR/.claude/mcp-operations.log"
exit 0
```
## Plugin Hooks
### Plugin Hook Configuration
Location: `plugin/hooks/hooks.json` or inline in `plugin.json`
```json
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [{
"type": "command",
"command": "${CLAUDE_PLUGIN_ROOT}/scripts/format-code.sh",
"timeout": 30
}]
}
]
}
}
```
### Plugin-Specific Variables
Use `${CLAUDE_PLUGIN_ROOT}` for plugin paths:
```json
{
"command": "${CLAUDE_PLUGIN_ROOT}/scripts/helper.sh"
}
```
### Plugin Hook Best Practices
**1. Use relative paths with variable:**
```json
{
"command": "${CLAUDE_PLUGIN_ROOT}/scripts/process.sh"
}
```
**2. Include dependencies in plugin:**
```
plugin/
├── .claude-plugin/
│ └── plugin.json
├── hooks/
│ └── hooks.json
└── scripts/
├── process.sh
└── utils.sh
```
**3. Document hook requirements:**
```json
{
"name": "my-plugin",
"description": "Plugin with auto-formatting",
"requirements": {
"binaries": ["jq", "black"],
"notes": "PostToolUse hooks require black for Python formatting"
}
}
```
## Advanced Patterns
### Conditional Execution
```bash
#!/usr/bin/env bash
set -euo pipefail
INPUT=$(cat)
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
# Only format during work hours
HOUR=$(date +%H)
if [[ $HOUR -lt 9 || $HOUR -gt 17 ]]; then
echo "Skipping format outside work hours"
exit 0
fi
# Only format if file is in src/
if [[ ! "$FILE_PATH" =~ ^.*/src/ ]]; then
exit 0
fi
# Format the file
black "$FILE_PATH"
```
### Async Operations
```bash
#!/usr/bin/env bash
# Run expensive operation in background
INPUT=$(cat)
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
# Start background job
(
sleep 2
expensive-operation "$FILE_PATH"
echo "Background operation completed" >> /tmp/claude-bg.log
) &
# Return immediately
echo "Background operation started"
exit 0
```
### State Management
```bash
#!/usr/bin/env bash
# Track state across hooks
STATE_FILE="/tmp/claude-state.json"
INPUT=$(cat)
TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name')
# Load state
if [[ -f "$STATE_FILE" ]]; then
STATE=$(cat "$STATE_FILE")
else
STATE='{"operations": []}'
fi
# Update state
STATE=$(echo "$STATE" | jq ".operations += [\"$TOOL_NAME\"]")
echo "$STATE" > "$STATE_FILE"
# Report
COUNT=$(echo "$STATE" | jq '.operations | length')
echo "Total operations: $COUNT"
exit 0
```
### Multi-File Operations
```bash
#!/usr/bin/env bash
# Update related files
INPUT=$(cat)
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
# If component updated, update index
if [[ "$FILE_PATH" =~ /components/.*\.tsx$ ]]; then
INDEX_FILE="$(dirname "$FILE_PATH")/index.ts"
# Regenerate index
echo "// Auto-generated by hook" > "$INDEX_FILE"
for file in "$(dirname "$FILE_PATH")"/*.tsx; do
NAME=$(basename "$file" .tsx)
echo "export { $NAME } from './$NAME';" >> "$INDEX_FILE"
done
echo "Updated $INDEX_FILE"
fi
exit 0
```
### Notification Integration
```bash
#!/usr/bin/env bash
# Send Slack notification
INPUT=$(cat)
TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name')
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
# Only notify for important files
if [[ "$FILE_PATH" =~ /src/core/ ]]; then
WEBHOOK_URL="${SLACK_WEBHOOK_URL:-}"
if [[ -n "$WEBHOOK_URL" ]]; then
curl -X POST "$WEBHOOK_URL" \
-H 'Content-Type: application/json' \
-d "{\"text\":\"Core file modified: $(basename "$FILE_PATH")\"}" \
2>/dev/null
fi
fi
exit 0
```
### Validation Pipeline
```bash
#!/usr/bin/env bash
# Multi-stage validation
INPUT=$(cat)
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty')
# Stage 1: Check for dangerous commands
if echo "$COMMAND" | grep -qE '\brm\s+-rf\s+/|\bmkfs\b|\bdd\s+if='; then
echo "❌ Dangerous command blocked" >&2
exit 2
fi
# Stage 2: Check for deprecated commands
if echo "$COMMAND" | grep -qE '\bgrep\b|\bfind\b'; then
echo "⚠ Consider using rg or fd instead" >&2
fi
# Stage 3: Check for common mistakes
if echo "$COMMAND" | grep -qE 'git\s+push\s+--force'; then
echo "⚠ Force push detected - use with caution" >&2
fi
exit 0
```
### Performance Monitoring
```bash
#!/usr/bin/env bash
# Track hook performance
START=$(date +%s%N)
# Hook logic here
INPUT=$(cat)
# ... process ...
# Calculate duration
END=$(date +%s%N)
DURATION=$(( (END - START) / 1000000 )) # milliseconds
# Log performance
echo "[$(date -Iseconds)] Hook duration: ${DURATION}ms" >> /tmp/claude-perf.log
exit 0
```
```
### references/examples.md
```markdown
# Hook Examples
Real-world examples of Claude Code event hooks for automation, validation, and workflow enhancement.
## Table of Contents
1. [Auto-Formatting](#auto-formatting)
2. [Validation Hooks](#validation-hooks)
3. [CI/CD Integration](#cicd-integration)
4. [Notification Systems](#notification-systems)
5. [Context Injection](#context-injection)
6. [Security Enforcement](#security-enforcement)
7. [Multi-Hook Workflows](#multi-hook-workflows)
8. [Team Collaboration](#team-collaboration)
9. [MCP Integration](#mcp-integration)
10. [Advanced Patterns](#advanced-patterns)
11. [Prompt-Based Hooks](#prompt-based-hooks)
12. [Community Examples](#community-examples)
13. [Component-Scoped Hooks](#component-scoped-hooks)
## Auto-Formatting
### TypeScript with Biome
Auto-format TypeScript files after writing or editing.
**Configuration** (`.claude/settings.json`):
```json
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit(*.ts|*.tsx)",
"hooks": [
{
"type": "command",
"command": "biome check --write \"$file\"",
"timeout": 10
}
]
}
]
}
}
```
**Result**: Every TypeScript file is automatically formatted with Biome after Claude writes or edits it.
### Python with Black
Auto-format Python files with Black.
**Configuration**:
```json
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit(*.py)",
"hooks": [
{
"type": "command",
"command": "black \"$file\"",
"timeout": 10
}
]
}
]
}
}
```
**Advanced version with multiple formatters**:
```json
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit(*.py)",
"hooks": [
{
"type": "command",
"command": "black \"$file\"",
"timeout": 10
},
{
"type": "command",
"command": "isort \"$file\"",
"timeout": 5
}
]
}
]
}
}
```
### Rust with rustfmt
Auto-format Rust code.
**Configuration**:
```json
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit(*.rs)",
"hooks": [
{
"type": "command",
"command": "rustfmt \"$file\"",
"timeout": 10
}
]
}
]
}
}
```
### Multi-Language Formatter
Format multiple languages with appropriate tools.
**Script** (`.claude/hooks/format-code.sh`):
```bash
#!/usr/bin/env bash
set -euo pipefail
INPUT=$(cat)
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
if [[ -z "$FILE_PATH" ]]; then
exit 0
fi
# Determine formatter based on extension
case "$FILE_PATH" in
*.ts|*.tsx|*.js|*.jsx)
if command -v biome &>/dev/null; then
biome check --write "$FILE_PATH" 2>&1 || true
fi
;;
*.py)
if command -v black &>/dev/null; then
black "$FILE_PATH" 2>&1 || true
isort "$FILE_PATH" 2>&1 || true
fi
;;
*.rs)
if command -v rustfmt &>/dev/null; then
rustfmt "$FILE_PATH" 2>&1 || true
fi
;;
*.go)
if command -v gofmt &>/dev/null; then
gofmt -w "$FILE_PATH" 2>&1 || true
fi
;;
*.md)
if command -v prettier &>/dev/null; then
prettier --write "$FILE_PATH" 2>&1 || true
fi
;;
esac
exit 0
```
**Configuration**:
```json
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "$CLAUDE_PROJECT_DIR/.claude/hooks/format-code.sh",
"timeout": 15
}
]
}
]
}
}
```
## Validation Hooks
### Bash Command Safety
Validate bash commands before execution.
**Script** (`.claude/hooks/validate-bash.sh`):
```bash
#!/usr/bin/env bash
set -euo pipefail
INPUT=$(cat)
TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name')
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty')
# Only validate Bash tool
if [[ "$TOOL_NAME" != "Bash" ]] || [[ -z "$COMMAND" ]]; then
exit 0
fi
# Dangerous patterns to block
DANGEROUS_PATTERNS=(
'\brm\s+-rf\s+/'
'\bmkfs\b'
'\bdd\s+if='
'\bformat\s+[cC]:'
'>\s*/dev/sd[a-z]'
'\b:()\{\s*:\|\:&\s*\};:' # Fork bomb
'\bchmod\s+777\s+/'
'\bchown\s+.*\s+/'
)
# Check for dangerous patterns
for pattern in "${DANGEROUS_PATTERNS[@]}"; do
if echo "$COMMAND" | grep -qE "$pattern"; then
cat << EOF >&2
❌ Dangerous command blocked
Command: $COMMAND
Pattern: $pattern
This command could cause system damage and has been blocked.
EOF
exit 2
fi
done
# Deprecated command warnings
if echo "$COMMAND" | grep -qE '\bgrep\b'; then
echo "⚠ Consider using 'rg' (ripgrep) instead of 'grep'" >&2
fi
if echo "$COMMAND" | grep -qE '\bfind\s+\S+\s+-name'; then
echo "⚠ Consider using 'rg --files' or 'fd' instead of 'find'" >&2
fi
# Force push warning
if echo "$COMMAND" | grep -qE 'git\s+push\s+(--force|-f)'; then
echo "⚠ Warning: Force push detected. Verify this is intentional." >&2
fi
echo "✓ Command validation passed"
exit 0
```
**Configuration**:
```json
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "$CLAUDE_PROJECT_DIR/.claude/hooks/validate-bash.sh",
"timeout": 5
}
]
}
]
}
}
```
### File Path Security
Prevent path traversal and sensitive file access.
**Script** (`.claude/hooks/validate-paths.sh`):
```bash
#!/usr/bin/env bash
set -euo pipefail
INPUT=$(cat)
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
if [[ -z "$FILE_PATH" ]]; then
exit 0
fi
# Check for path traversal
if echo "$FILE_PATH" | grep -qE '\.\./'; then
cat << EOF >&2
❌ Path traversal detected
File: $FILE_PATH
Paths containing '..' are not allowed for security reasons.
EOF
exit 2
fi
# Check for sensitive system paths
SENSITIVE_PATTERNS=(
'^/etc/'
'^/root/'
'^/home/[^/]+/\.ssh/'
'^/var/log/'
'^/sys/'
'^/proc/'
)
for pattern in "${SENSITIVE_PATTERNS[@]}"; do
if echo "$FILE_PATH" | grep -qE "$pattern"; then
cat << EOF >&2
❌ Access to sensitive system path blocked
File: $FILE_PATH
This path is restricted for security reasons.
EOF
exit 2
fi
done
# Check for sensitive files
SENSITIVE_FILES=(
'\.env$'
'\.env\.'
'id_rsa'
'id_ed25519'
'\.pem$'
'credentials'
'password'
'\.key$'
'secret'
)
for pattern in "${SENSITIVE_FILES[@]}"; do
if echo "$FILE_PATH" | grep -qiE "$pattern"; then
cat << EOF >&2
⚠ Warning: Accessing sensitive file
File: $FILE_PATH
This file may contain sensitive information. Ensure this is intentional.
EOF
# Don't block, just warn
fi
done
exit 0
```
**Configuration**:
```json
{
"hooks": {
"PreToolUse": [
{
"matcher": "Write|Edit|Read",
"hooks": [
{
"type": "command",
"command": "$CLAUDE_PROJECT_DIR/.claude/hooks/validate-paths.sh",
"timeout": 3
}
]
}
]
}
}
```
### JSON Schema Validation
Validate JSON files against schemas.
**Script** (`.claude/hooks/validate-json.sh`):
```bash
#!/usr/bin/env bash
set -euo pipefail
INPUT=$(cat)
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
CONTENT=$(echo "$INPUT" | jq -r '.tool_input.content // empty')
# Only validate JSON files
if [[ ! "$FILE_PATH" =~ \.json$ ]] || [[ -z "$CONTENT" ]]; then
exit 0
fi
# Validate JSON syntax
if ! echo "$CONTENT" | jq empty 2>/dev/null; then
echo "❌ Invalid JSON syntax" >&2
exit 2
fi
# Validate specific files against schemas
case "$FILE_PATH" in
*package.json)
# Validate package.json has required fields
if ! echo "$CONTENT" | jq -e '.name and .version' >/dev/null 2>&1; then
echo "⚠ package.json missing required fields (name, version)" >&2
fi
;;
*tsconfig.json)
# Validate tsconfig has compilerOptions
if ! echo "$CONTENT" | jq -e '.compilerOptions' >/dev/null 2>&1; then
echo "⚠ tsconfig.json missing compilerOptions" >&2
fi
;;
*.claude/settings.json)
# Validate hooks structure if present
if echo "$CONTENT" | jq -e '.hooks' >/dev/null 2>&1; then
# Check each hook has required fields
if ! echo "$CONTENT" | jq -e '.hooks | to_entries[] | .value[] | .matcher and .hooks' >/dev/null 2>&1; then
echo "❌ Invalid hooks configuration" >&2
exit 2
fi
fi
;;
esac
echo "✓ JSON validation passed"
exit 0
```
**Configuration**:
```json
{
"hooks": {
"PreToolUse": [
{
"matcher": "Write(*.json)",
"hooks": [
{
"type": "command",
"command": "$CLAUDE_PROJECT_DIR/.claude/hooks/validate-json.sh",
"timeout": 5
}
]
}
]
}
}
```
## CI/CD Integration
### Trigger Build on File Change
Trigger build when specific files are modified.
**Script** (`.claude/hooks/trigger-build.sh`):
```bash
#!/usr/bin/env bash
set -euo pipefail
INPUT=$(cat)
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
# Only trigger for source files
if [[ ! "$FILE_PATH" =~ (src/|lib/|pages/) ]]; then
exit 0
fi
# Check if CI/CD is configured
if [[ ! -f ".github/workflows/build.yml" ]] && [[ ! -f ".gitlab-ci.yml" ]]; then
exit 0
fi
# Create marker file for build trigger
MARKER_FILE=".build-needed"
echo "Build triggered by: $FILE_PATH" >> "$MARKER_FILE"
echo "✓ Build marker created: $MARKER_FILE"
echo "Run 'git add $MARKER_FILE && git commit' to trigger CI/CD build"
exit 0
```
**Configuration**:
```json
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "$CLAUDE_PROJECT_DIR/.claude/hooks/trigger-build.sh",
"timeout": 2
}
]
}
]
}
}
```
### Run Tests After Code Changes
Automatically run tests when code changes.
**Script** (`.claude/hooks/run-tests.sh`):
```bash
#!/usr/bin/env bash
set -euo pipefail
INPUT=$(cat)
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
# Only run tests for source files
if [[ ! "$FILE_PATH" =~ \.(ts|tsx|js|jsx|py|rs)$ ]]; then
exit 0
fi
# Skip test files themselves
if [[ "$FILE_PATH" =~ \.(test|spec)\. ]]; then
exit 0
fi
echo "Running tests..."
# Detect test runner and run tests
if [[ -f "package.json" ]] && grep -q '"test"' package.json; then
# Node.js project
if command -v bun &>/dev/null; then
bun test 2>&1 || {
echo "⚠ Tests failed. Review failures before committing." >&2
exit 0 # Don't block, just warn
}
elif command -v npm &>/dev/null; then
npm test 2>&1 || {
echo "⚠ Tests failed. Review failures before committing." >&2
exit 0
}
fi
elif [[ -f "Cargo.toml" ]]; then
# Rust project
cargo test 2>&1 || {
echo "⚠ Tests failed. Review failures before committing." >&2
exit 0
}
elif [[ -f "pytest.ini" ]] || [[ -f "pyproject.toml" ]]; then
# Python project
pytest 2>&1 || {
echo "⚠ Tests failed. Review failures before committing." >&2
exit 0
}
fi
echo "✓ Tests passed"
exit 0
```
**Configuration**:
```json
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit(*.ts|*.tsx|*.py|*.rs)",
"hooks": [
{
"type": "command",
"command": "$CLAUDE_PROJECT_DIR/.claude/hooks/run-tests.sh",
"timeout": 60
}
]
}
]
}
}
```
### Update Documentation
Auto-update docs when code changes.
**Script** (`.claude/hooks/update-docs.sh`):
```bash
#!/usr/bin/env bash
set -euo pipefail
INPUT=$(cat)
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
# Only for public API files
if [[ ! "$FILE_PATH" =~ src/(index|api|public)\. ]]; then
exit 0
fi
# Generate TypeScript docs
if [[ -f "tsconfig.json" ]] && command -v typedoc &>/dev/null; then
echo "Generating TypeScript documentation..."
typedoc --out docs/api src/index.ts 2>&1 || true
fi
# Generate Python docs
if [[ "$FILE_PATH" =~ \.py$ ]] && command -v pdoc &>/dev/null; then
echo "Generating Python documentation..."
pdoc --html --force --output-dir docs/api . 2>&1 || true
fi
# Generate Rust docs
if [[ "$FILE_PATH" =~ \.rs$ ]] && [[ -f "Cargo.toml" ]]; then
echo "Generating Rust documentation..."
cargo doc --no-deps 2>&1 || true
fi
exit 0
```
**Configuration**:
```json
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "$CLAUDE_PROJECT_DIR/.claude/hooks/update-docs.sh",
"timeout": 30
}
]
}
]
}
}
```
## Notification Systems
### Slack Integration
Send notifications to Slack.
**Script** (`.claude/hooks/notify-slack.sh`):
```bash
#!/usr/bin/env bash
set -euo pipefail
# Load webhook URL from .env if present (safe single-variable extraction)
if [[ -f ".env" ]]; then
SLACK_WEBHOOK_URL=$(grep -E '^SLACK_WEBHOOK_URL=' .env | cut -d'=' -f2- | tr -d '"' | tr -d "'")
fi
WEBHOOK_URL="${SLACK_WEBHOOK_URL:-}"
if [[ -z "$WEBHOOK_URL" ]]; then
exit 0
fi
INPUT=$(cat)
TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name')
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
# Only notify for important operations
case "$TOOL_NAME" in
Write|Edit)
# Only notify for specific directories
if [[ "$FILE_PATH" =~ (src/core/|src/api/|migrations/) ]]; then
MESSAGE="🤖 Claude modified: \`$(basename "$FILE_PATH")\` in \`$(dirname "$FILE_PATH")\`"
curl -X POST "$WEBHOOK_URL" \
-H 'Content-Type: application/json' \
-d "{\"text\":\"$MESSAGE\"}" \
--silent --show-error 2>&1 || true
fi
;;
esac
exit 0
```
**Configuration**:
```json
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "$CLAUDE_PROJECT_DIR/.claude/hooks/notify-slack.sh",
"timeout": 10
}
]
}
]
}
}
```
### Email Notifications
Send email for important events.
> **Note**: Configure a valid email address before use. The `mail` command will
> succeed even if the email is undeliverable, so verify your mail server setup.
**Script** (`.claude/hooks/send-email.sh`):
```bash
#!/usr/bin/env bash
set -euo pipefail
INPUT=$(cat)
HOOK_EVENT=$(echo "$INPUT" | jq -r '.hook_event_name')
# Only email for session events
if [[ "$HOOK_EVENT" != "SessionEnd" ]]; then
exit 0
fi
REASON=$(echo "$INPUT" | jq -r '.reason // "unknown"')
SESSION_ID=$(echo "$INPUT" | jq -r '.session_id')
# Check if email is configured
if ! command -v mail &>/dev/null; then
exit 0
fi
# Send email
mail -s "Claude Code Session Ended: $REASON" [email protected] << EOF
Session ID: $SESSION_ID
Reason: $REASON
Timestamp: $(date -Iseconds)
This is an automated notification from Claude Code.
EOF
exit 0
```
**Configuration**:
```json
{
"hooks": {
"SessionEnd": [
{
"matcher": "*",
"hooks": [
{
"type": "command",
"command": "$CLAUDE_PROJECT_DIR/.claude/hooks/send-email.sh",
"timeout": 5
}
]
}
]
}
}
```
### Logging System
Comprehensive logging of all operations.
**Script** (`.claude/hooks/log-operations.sh`):
```bash
#!/usr/bin/env bash
set -euo pipefail
LOG_DIR="$CLAUDE_PROJECT_DIR/.claude/logs"
mkdir -p "$LOG_DIR"
INPUT=$(cat)
TIMESTAMP=$(date -Iseconds)
HOOK_EVENT=$(echo "$INPUT" | jq -r '.hook_event_name')
TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // "N/A"')
SESSION_ID=$(echo "$INPUT" | jq -r '.session_id')
# Log to daily file
LOG_FILE="$LOG_DIR/$(date +%Y-%m-%d).log"
# Create log entry
LOG_ENTRY=$(jq -n \
--arg ts "$TIMESTAMP" \
--arg event "$HOOK_EVENT" \
--arg tool "$TOOL_NAME" \
--arg session "$SESSION_ID" \
'{timestamp: $ts, event: $event, tool: $tool, session: $session}')
echo "$LOG_ENTRY" >> "$LOG_FILE"
# Rotate logs older than 30 days
find "$LOG_DIR" -name "*.log" -mtime +30 -delete 2>/dev/null || true
exit 0
```
**Configuration**:
```json
{
"hooks": {
"PreToolUse": [
{
"matcher": "*",
"hooks": [
{
"type": "command",
"command": "$CLAUDE_PROJECT_DIR/.claude/hooks/log-operations.sh",
"timeout": 2
}
]
}
]
}
}
```
## Context Injection
### Add Timestamp
Add current timestamp to every prompt.
**Script** (`.claude/hooks/add-timestamp.sh`):
```bash
#!/usr/bin/env bash
set -euo pipefail
# Output current time context
cat << EOF
Current timestamp: $(date -Iseconds)
Current time: $(date '+%Y-%m-%d %H:%M:%S %Z')
Day of week: $(date '+%A')
EOF
exit 0
```
**Configuration**:
```json
{
"hooks": {
"UserPromptSubmit": [
{
"matcher": "*",
"hooks": [
{
"type": "command",
"command": "$CLAUDE_PROJECT_DIR/.claude/hooks/add-timestamp.sh",
"timeout": 1
}
]
}
]
}
}
```
### Add Git Context
Inject git status into prompt context.
**Script** (`.claude/hooks/git-context.sh`):
```bash
#!/usr/bin/env bash
set -euo pipefail
# Check if git repo
if ! git rev-parse --git-dir &>/dev/null; then
exit 0
fi
cat << EOF
## Git Context
Branch: $(git branch --show-current 2>/dev/null || echo "detached")
Status: $(git status --short 2>/dev/null | wc -l | xargs) files modified
Last commit: $(git log -1 --oneline 2>/dev/null || echo "none")
Remote: $(git remote -v 2>/dev/null | head -1 | awk '{print $2}' || echo "none")
EOF
exit 0
```
**Configuration**:
```json
{
"hooks": {
"SessionStart": [
{
"matcher": "startup",
"hooks": [
{
"type": "command",
"command": "$CLAUDE_PROJECT_DIR/.claude/hooks/git-context.sh",
"timeout": 3
}
]
}
]
}
}
```
### Add Environment Info
Inject environment and system information.
**Script** (`.claude/hooks/env-context.sh`):
```bash
#!/usr/bin/env bash
set -euo pipefail
cat << EOF
## Environment Context
OS: $(uname -s)
Architecture: $(uname -m)
Node: $(node --version 2>/dev/null || echo "not installed")
Bun: $(bun --version 2>/dev/null || echo "not installed")
Python: $(python3 --version 2>/dev/null || echo "not installed")
Rust: $(rustc --version 2>/dev/null || echo "not installed")
Working directory: $PWD
User: $USER
Shell: $SHELL
EOF
exit 0
```
**Configuration**:
```json
{
"hooks": {
"SessionStart": [
{
"matcher": "startup",
"hooks": [
{
"type": "command",
"command": "$CLAUDE_PROJECT_DIR/.claude/hooks/env-context.sh",
"timeout": 2
}
]
}
]
}
}
```
## Security Enforcement
### Block Sensitive File Operations
Prevent operations on sensitive files.
**Script** (`.claude/hooks/block-sensitive.sh`):
```bash
#!/usr/bin/env bash
set -euo pipefail
INPUT=$(cat)
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
if [[ -z "$FILE_PATH" ]]; then
exit 0
fi
# Define sensitive patterns
BLOCKED_PATTERNS=(
'\.env$'
'\.env\.'
'credentials'
'secrets'
'id_rsa'
'id_ed25519'
'\.pem$'
'\.key$'
'\.p12$'
'password'
'token'
'\.git/config$'
)
# Check against patterns
for pattern in "${BLOCKED_PATTERNS[@]}"; do
if echo "$FILE_PATH" | grep -qiE "$pattern"; then
cat << EOF >&2
❌ Access to sensitive file blocked
File: $FILE_PATH
Pattern: $pattern
This file may contain sensitive information and is protected.
If you need to modify this file, do it manually outside Claude Code.
EOF
exit 2
fi
done
exit 0
```
**Configuration**:
```json
{
"hooks": {
"PreToolUse": [
{
"matcher": "Write|Edit|Read",
"hooks": [
{
"type": "command",
"command": "$CLAUDE_PROJECT_DIR/.claude/hooks/block-sensitive.sh",
"timeout": 2
}
]
}
]
}
}
```
### Enforce File Permissions
Ensure proper file permissions.
**Script** (`.claude/hooks/enforce-permissions.sh`):
```bash
#!/usr/bin/env bash
set -euo pipefail
INPUT=$(cat)
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
if [[ -z "$FILE_PATH" ]] || [[ ! -f "$FILE_PATH" ]]; then
exit 0
fi
# Ensure no world-writable files
if [[ -w "$FILE_PATH" ]] && [[ $(stat -f %A "$FILE_PATH" 2>/dev/null || stat -c %a "$FILE_PATH" 2>/dev/null) =~ [0-9][0-9]2$ ]]; then
chmod o-w "$FILE_PATH"
echo "⚠ Removed world-write permission from $FILE_PATH" >&2
fi
# Ensure scripts are executable
if [[ "$FILE_PATH" =~ \.(sh|bash|zsh)$ ]]; then
if [[ ! -x "$FILE_PATH" ]]; then
chmod +x "$FILE_PATH"
echo "✓ Made script executable: $FILE_PATH"
fi
fi
exit 0
```
**Configuration**:
```json
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write",
"hooks": [
{
"type": "command",
"command": "$CLAUDE_PROJECT_DIR/.claude/hooks/enforce-permissions.sh",
"timeout": 3
}
]
}
]
}
}
```
### Audit Trail
Create audit trail of all operations.
**Script** (`.claude/hooks/audit-trail.sh`):
```bash
#!/usr/bin/env bash
set -euo pipefail
AUDIT_FILE="$CLAUDE_PROJECT_DIR/.claude/audit.log"
INPUT=$(cat)
TIMESTAMP=$(date -Iseconds)
HOOK_EVENT=$(echo "$INPUT" | jq -r '.hook_event_name')
TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // "N/A"')
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // "N/A"')
SESSION_ID=$(echo "$INPUT" | jq -r '.session_id')
# Create audit entry
AUDIT_ENTRY=$(jq -n \
--arg ts "$TIMESTAMP" \
--arg event "$HOOK_EVENT" \
--arg tool "$TOOL_NAME" \
--arg file "$FILE_PATH" \
--arg session "$SESSION_ID" \
--arg user "$USER" \
--arg host "$HOSTNAME" \
'{
timestamp: $ts,
event: $event,
tool: $tool,
file: $file,
session: $session,
user: $user,
host: $host
}')
# Append to audit log
echo "$AUDIT_ENTRY" >> "$AUDIT_FILE"
# Keep only last 10000 lines
tail -n 10000 "$AUDIT_FILE" > "$AUDIT_FILE.tmp" && mv "$AUDIT_FILE.tmp" "$AUDIT_FILE"
exit 0
```
**Configuration**:
```json
{
"hooks": {
"PreToolUse": [
{
"matcher": "*",
"hooks": [
{
"type": "command",
"command": "$CLAUDE_PROJECT_DIR/.claude/hooks/audit-trail.sh",
"timeout": 2
}
]
}
]
}
}
```
## Multi-Hook Workflows
### Complete TypeScript Workflow
Format, type-check, lint, and test TypeScript files.
**Configuration**:
```json
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit(*.ts|*.tsx)",
"hooks": [
{
"type": "command",
"command": "biome check --write \"$file\"",
"timeout": 10
},
{
"type": "command",
"command": "tsc --noEmit \"$file\"",
"timeout": 15
},
{
"type": "command",
"command": "$CLAUDE_PROJECT_DIR/.claude/hooks/run-tests.sh",
"timeout": 30
}
]
}
]
}
}
```
### Python Development Workflow
Format, type-check, lint, and test Python files.
**Configuration**:
```json
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit(*.py)",
"hooks": [
{
"type": "command",
"command": "black \"$file\"",
"timeout": 10
},
{
"type": "command",
"command": "isort \"$file\"",
"timeout": 5
},
{
"type": "command",
"command": "mypy \"$file\"",
"timeout": 10
},
{
"type": "command",
"command": "pylint \"$file\"",
"timeout": 15
}
]
}
]
}
}
```
### Pre-Commit Workflow
Validate before allowing write operations.
**Configuration**:
```json
{
"hooks": {
"PreToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "$CLAUDE_PROJECT_DIR/.claude/hooks/validate-paths.sh",
"timeout": 3
},
{
"type": "command",
"command": "$CLAUDE_PROJECT_DIR/.claude/hooks/block-sensitive.sh",
"timeout": 2
}
]
}
],
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "$CLAUDE_PROJECT_DIR/.claude/hooks/format-code.sh",
"timeout": 15
},
{
"type": "command",
"command": "$CLAUDE_PROJECT_DIR/.claude/hooks/audit-trail.sh",
"timeout": 2
}
]
}
]
}
}
```
## Team Collaboration
### Shared Team Hooks
Team-wide formatting and validation.
**Project** (`.claude/settings.json`):
```json
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit(*.ts|*.tsx)",
"hooks": [
{
"type": "command",
"command": "biome check --write \"$file\"",
"timeout": 10
}
]
},
{
"matcher": "Write|Edit(*.py)",
"hooks": [
{
"type": "command",
"command": "black \"$file\"",
"timeout": 10
}
]
}
],
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "$CLAUDE_PROJECT_DIR/.claude/hooks/validate-bash.sh",
"timeout": 5
}
]
}
]
}
}
```
### Personal Overrides
Personal preferences that extend team hooks.
**Personal** (`~/.claude/settings.json`):
```json
{
"hooks": {
"SessionStart": [
{
"matcher": "startup",
"hooks": [
{
"type": "command",
"command": "echo '👋 Welcome back!' && git status",
"timeout": 3
}
]
}
],
"Stop": [
{
"matcher": "*",
"hooks": [
{
"type": "command",
"command": "echo '✅ Task completed at $(date +%H:%M)'",
"timeout": 1
}
]
}
]
}
}
```
## MCP Integration
### Log Memory Operations
Track MCP memory tool usage.
**Script** (`.claude/hooks/log-memory.sh`):
```bash
#!/usr/bin/env bash
set -euo pipefail
INPUT=$(cat)
TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name')
# Only for memory MCP tools
if [[ ! "$TOOL_NAME" =~ ^mcp__memory__ ]]; then
exit 0
fi
OPERATION=$(echo "$TOOL_NAME" | sed 's/mcp__memory__//')
TIMESTAMP=$(date -Iseconds)
# Log the operation
echo "[$TIMESTAMP] Memory operation: $OPERATION" >> "$CLAUDE_PROJECT_DIR/.claude/memory-ops.log"
exit 0
```
**Configuration**:
```json
{
"hooks": {
"PreToolUse": [
{
"matcher": "mcp__memory__.*",
"hooks": [
{
"type": "command",
"command": "$CLAUDE_PROJECT_DIR/.claude/hooks/log-memory.sh",
"timeout": 2
}
]
}
]
}
}
```
### Validate GitHub Operations
Validate GitHub MCP operations.
**Script** (`.claude/hooks/validate-github.sh`):
```bash
#!/usr/bin/env bash
set -euo pipefail
INPUT=$(cat)
TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name')
# Only for GitHub MCP tools
if [[ ! "$TOOL_NAME" =~ ^mcp__github__ ]]; then
exit 0
fi
# Warn about destructive operations
if [[ "$TOOL_NAME" =~ (delete|close|merge) ]]; then
echo "⚠ Warning: Destructive GitHub operation: $TOOL_NAME" >&2
fi
exit 0
```
**Configuration**:
```json
{
"hooks": {
"PreToolUse": [
{
"matcher": "mcp__github__.*",
"hooks": [
{
"type": "command",
"command": "$CLAUDE_PROJECT_DIR/.claude/hooks/validate-github.sh",
"timeout": 2
}
]
}
]
}
}
```
## Advanced Patterns
### Conditional Hook Execution
Execute hooks only under certain conditions.
**Script** (`.claude/hooks/conditional-format.sh`):
```bash
#!/usr/bin/env bash
set -euo pipefail
INPUT=$(cat)
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
# Only format during work hours (9 AM - 5 PM)
HOUR=$(date +%H)
if [[ $HOUR -lt 9 || $HOUR -gt 17 ]]; then
echo "Skipping format outside work hours"
exit 0
fi
# Only format files in src/ directory
if [[ ! "$FILE_PATH" =~ ^.*/src/ ]]; then
exit 0
fi
# Run formatter
if [[ "$FILE_PATH" =~ \.ts$ ]]; then
biome check --write "$FILE_PATH"
fi
exit 0
```
### State Management
Track state across hook invocations.
**Script** (`.claude/hooks/track-changes.sh`):
```bash
#!/usr/bin/env bash
set -euo pipefail
STATE_FILE="$CLAUDE_PROJECT_DIR/.claude/hook-state.json"
# Initialize state if needed
if [[ ! -f "$STATE_FILE" ]]; then
echo '{"files_modified": [], "total_operations": 0}' > "$STATE_FILE"
fi
INPUT=$(cat)
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
if [[ -n "$FILE_PATH" ]]; then
# Update state
STATE=$(cat "$STATE_FILE")
STATE=$(echo "$STATE" | jq \
--arg file "$FILE_PATH" \
'.files_modified += [$file] | .files_modified |= unique | .total_operations += 1')
echo "$STATE" > "$STATE_FILE"
# Report
TOTAL=$(echo "$STATE" | jq '.total_operations')
echo "Total operations this session: $TOTAL"
fi
exit 0
```
### Async Background Operations
Run expensive operations asynchronously.
**Script** (`.claude/hooks/async-index.sh`):
```bash
#!/usr/bin/env bash
set -euo pipefail
INPUT=$(cat)
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
# Start background indexing
(
sleep 1
# Rebuild search index
if command -v rg &>/dev/null; then
rg --files > "$CLAUDE_PROJECT_DIR/.claude/file-index.txt" 2>/dev/null
fi
echo "Index updated: $(date -Iseconds)" >> "$CLAUDE_PROJECT_DIR/.claude/index.log"
) &
echo "✓ Background indexing started"
exit 0
```
### Performance Monitoring
Track hook performance.
**Script** (`.claude/hooks/perf-monitor.sh`):
```bash
#!/usr/bin/env bash
set -euo pipefail
START_NS=$(date +%s%N)
# Original hook logic
INPUT=$(cat)
TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name')
# ... process ...
# Calculate duration
END_NS=$(date +%s%N)
DURATION_MS=$(( (END_NS - START_NS) / 1000000 ))
# Log performance
PERF_LOG="$CLAUDE_PROJECT_DIR/.claude/perf.log"
echo "$(date -Iseconds) | $TOOL_NAME | ${DURATION_MS}ms" >> "$PERF_LOG"
# Warn if slow
if [[ $DURATION_MS -gt 1000 ]]; then
echo "⚠ Hook took ${DURATION_MS}ms (>1s)" >&2
fi
exit 0
```
### Error Recovery
Robust error handling with recovery.
**Script** (`.claude/hooks/robust-format.sh`):
```bash
#!/usr/bin/env bash
set -euo pipefail
# Trap errors
trap 'echo "Error on line $LINENO" >&2; exit 1' ERR
INPUT=$(cat)
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
if [[ -z "$FILE_PATH" ]]; then
exit 0
fi
# Create backup before formatting
BACKUP="${FILE_PATH}.bak"
cp "$FILE_PATH" "$BACKUP"
# Try to format
if ! biome check --write "$FILE_PATH" 2>/dev/null; then
# Restore backup on failure
mv "$BACKUP" "$FILE_PATH"
echo "⚠ Format failed, restored original file" >&2
exit 1
fi
# Remove backup on success
rm -f "$BACKUP"
echo "✓ Formatted successfully"
exit 0
```
## Prompt-Based Hooks
Prompt-based hooks use LLM reasoning for context-aware validation. Recommended for complex decisions.
### Smart Security Validation
Use LLM to analyze file operations:
```json
{
"hooks": {
"PreToolUse": [
{
"matcher": "Write|Edit",
"hooks": [{
"type": "prompt",
"prompt": "Analyze this file operation for security issues:\n\n$TOOL_INPUT\n\nCheck for:\n1. Sensitive paths (/etc, ~/.ssh, .env files)\n2. Credentials or API keys in content\n3. Path traversal attempts (..)\n4. Executable file creation\n\nRespond with JSON: {\"decision\": \"allow|deny\", \"reason\": \"brief explanation\"}",
"timeout": 30
}]
}
]
}
}
```
### Context-Aware Bash Validation
Evaluate command safety with reasoning:
```json
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [{
"type": "prompt",
"prompt": "Evaluate if this bash command is safe to execute:\n\n$TOOL_INPUT\n\nConsider:\n1. Could it delete important files?\n2. Could it expose secrets?\n3. Could it modify system configuration?\n4. Is it appropriate for a development environment?\n\nRespond: {\"decision\": \"allow|deny\", \"reason\": \"...\"}",
"timeout": 30
}]
}
]
}
}
```
### Task Completion Verification
Verify work quality before stopping:
```json
{
"hooks": {
"Stop": [
{
"matcher": "*",
"hooks": [{
"type": "prompt",
"prompt": "Review the completed task. Consider:\n1. Were all requirements addressed?\n2. Were tests added or updated?\n3. Is there any unfinished work?\n4. Should the user be informed of anything?\n\nProvide a brief summary if there are concerns.",
"timeout": 30
}]
}
]
}
}
```
## Community Examples
Real-world examples from the Claude Code community.
### disler/claude-code-hooks-mastery
Comprehensive hook examples using Python with UV for dependency management.
**Directory structure**:
```
.claude/
├── hooks/
│ ├── user_prompt_submit.py # Prompt validation and logging
│ ├── pre_tool_use.py # Command blocking
│ ├── post_tool_use.py # Tool completion logging
│ ├── notification.py # TTS notifications
│ ├── stop.py # AI completion messages
│ ├── subagent_stop.py # Subagent tracking
│ ├── pre_compact.py # Transcript backup
│ └── session_start.py # Context loading
└── settings.json
```
**Configuration pattern**:
```json
{
"UserPromptSubmit": [{
"hooks": [{
"type": "command",
"command": "uv run .claude/hooks/user_prompt_submit.py --log-only"
}]
}],
"PreToolUse": [{
"matcher": "Bash",
"hooks": [{
"type": "command",
"command": "uv run .claude/hooks/pre_tool_use.py"
}]
}]
}
```
**Source**: https://github.com/disler/claude-code-hooks-mastery
### ChrisWiles/claude-code-showcase
Complete Claude Code configuration with hooks, skills, agents, and GitHub Actions.
**Features**:
- Auto-format code on file changes
- Run tests when test files change
- Type-check TypeScript
- Block edits on main branch
- Skill matching for prompts
**Branch protection hook**:
```json
{
"hooks": {
"PreToolUse": [{
"matcher": "Edit|Write",
"hooks": [{
"type": "command",
"command": "[ \"$(git branch --show-current)\" != \"main\" ] || exit 2",
"timeout": 5
}]
}]
}
}
```
**Source**: https://github.com/ChrisWiles/claude-code-showcase
### GitButler Integration
GitButler provides hooks for automatic branch and commit management.
**Configuration**:
```json
{
"hooks": {
"PreToolUse": [{
"matcher": "*",
"hooks": [{
"type": "command",
"command": "but claude pre-tool",
"timeout": 5
}]
}],
"PostToolUse": [{
"matcher": "*",
"hooks": [{
"type": "command",
"command": "but claude post-tool",
"timeout": 5
}]
}],
"Stop": [{
"matcher": "*",
"hooks": [{
"type": "command",
"command": "but claude stop",
"timeout": 10
}]
}]
}
}
```
**Source**: https://docs.gitbutler.com/features/ai-integration/claude-code-hooks
## Component-Scoped Hooks
Hooks defined in skills, agents, and commands frontmatter. Only active when the component is loaded.
### Skill with Validation Hook
```yaml
---
name: secure-coding
description: Security-focused coding skill
hooks:
PreToolUse:
- matcher: "Write|Edit"
hooks:
- type: prompt
prompt: "Validate this code change for security best practices..."
PostToolUse:
- matcher: "Write|Edit(*.ts)"
hooks:
- type: command
command: "eslint --fix \"$file\""
---
# Secure Coding Skill
When active, this skill validates all code changes for security issues.
```
### Agent with Completion Hook
```yaml
---
name: code-reviewer
description: Reviews code for quality issues
model: sonnet
hooks:
Stop:
- matcher: "*"
hooks:
- type: prompt
prompt: "Summarize the code review findings and severity levels."
---
# Code Reviewer Agent
Performs thorough code review with summarized findings.
```
### Command with Context Hook
```yaml
---
description: Deploy to staging environment
argument-hint: [component to deploy]
hooks:
PreToolUse:
- matcher: "Bash"
hooks:
- type: command
command: "./.claude/hooks/validate-deploy.sh"
---
# Deploy Command
Deploys the specified component to staging with pre-flight checks.
```
## External Resources
- [Official Hooks Reference](https://code.claude.com/docs/en/hooks)
- [Hooks Guide](https://code.claude.com/docs/en/hooks-guide)
- [Claude Code Blog: Hook Configuration](https://claude.com/blog/how-to-configure-hooks)
- [disler/claude-code-hooks-mastery](https://github.com/disler/claude-code-hooks-mastery)
- [ChrisWiles/claude-code-showcase](https://github.com/ChrisWiles/claude-code-showcase)
- [GitButler Hooks Documentation](https://docs.gitbutler.com/features/ai-integration/claude-code-hooks)
```