playbook/outfitter-agents/plugins/outfitter/skills/claude-hooks/references/schema.md

1793 lines
32 KiB
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
### 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
Hooks are **auto-discovered** from `{plugin}/hooks/hooks.json`. Do NOT define hooks in `plugin.json`.
**Location:** `{plugin}/hooks/hooks.json`
**Important:** The file requires a root-level `"hooks"` wrapper around the event types:
```json
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [{
"type": "command",
"command": "${CLAUDE_PLUGIN_ROOT}/scripts/format-code.sh",
"timeout": 30
}]
}
]
}
}
```
> **Note:** This differs from project-level hooks in `.claude/settings.json`, which also have a `"hooks"` wrapper but are configured differently. Plugin hooks use `${CLAUDE_PLUGIN_ROOT}` for paths.
### 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
```