909 lines
21 KiB
Markdown
909 lines
21 KiB
Markdown
---
|
|
name: box-factory-hooks-design
|
|
description: Interpretive guidance for designing Claude Code hooks. Helps you understand hook lifecycle, when to use hooks vs other patterns, and common pitfalls. Use when creating or reviewing hooks.
|
|
---
|
|
|
|
# Hooks Design Skill
|
|
|
|
This skill provides interpretive guidance and best practices for creating Claude Code hooks. **ALWAYS fetch current official documentation before creating hooks** - this skill helps you understand what the docs mean and how to create excellent hooks.
|
|
|
|
## Required Reading Before Creating Hooks
|
|
|
|
Fetch these docs with WebFetch every time:
|
|
|
|
- **https://code.claude.com/docs/en/hooks** - Complete hook reference
|
|
|
|
## Core Understanding
|
|
|
|
### Hooks Are Deterministic Control
|
|
|
|
**Key insight:** Hooks provide guaranteed, deterministic execution at specific lifecycle events.
|
|
|
|
**What this means:**
|
|
|
|
- **Hooks** = Execute every single time (deterministic)
|
|
- **Prompts/Instructions** = Claude might forget (probabilistic)
|
|
- **Agents** = Context-dependent intelligence
|
|
- **Use hooks when "always" matters**
|
|
|
|
**Decision question:** Do you need this to happen every single time, or is "usually" okay?
|
|
|
|
**Examples:**
|
|
|
|
- Format after every file write → Hook
|
|
- Suggest code improvements → Prompt to Claude
|
|
- Run tests after code changes → Hook if mandatory, agent if discretionary
|
|
- Security validation before bash → Hook (must be enforced)
|
|
|
|
### Hook Architecture (How It Fits)
|
|
|
|
**Execution flow:**
|
|
|
|
```
|
|
User Input → Claude Thinks → Tool Execution
|
|
↑ ↓
|
|
Hooks fire here ────┘
|
|
```
|
|
|
|
**Critical implications:**
|
|
|
|
- **PreToolUse**: Can block/modify before tool runs
|
|
- **PostToolUse**: Can react after tool completes successfully
|
|
- **Stop**: Can't affect what Claude just did (it's done, only cleanup)
|
|
- **UserPromptSubmit**: Runs before Claude sees the prompt
|
|
|
|
### Exit Codes Are Communication (Official Specification)
|
|
|
|
**Exit 0**: Hook succeeded, continue execution
|
|
|
|
- stdout displays in transcript mode (CTRL-R)
|
|
- Exception: `UserPromptSubmit` and `SessionStart` where stdout becomes context for Claude
|
|
|
|
**Exit 2**: Blocking error, stop and handle
|
|
|
|
- stderr feeds to Claude for processing
|
|
- Behavior varies by event:
|
|
- **PreToolUse**: Blocks tool call
|
|
- **Stop/SubagentStop**: Blocks stoppage
|
|
- **UserPromptSubmit**: Blocks prompt, erases it, shows error to user only
|
|
|
|
**Other exit codes**: Non-blocking error
|
|
|
|
- stderr displays to user
|
|
- Execution continues
|
|
|
|
**Best practice:** Use exit 2 sparingly - it's powerful but disruptive. Use it for security/safety enforcement, not preferences.
|
|
|
|
## Hook Events (Official Specification)
|
|
|
|
Complete list of available events:
|
|
|
|
| Event | When It Fires | Matcher Applies |
|
|
|-------|--------------|----------------|
|
|
| **PreToolUse** | After Claude creates tool params, before processing | Yes |
|
|
| **PostToolUse** | Immediately after successful tool completion | Yes |
|
|
| **PermissionRequest** | When permission dialogs shown to users | No |
|
|
| **Notification** | When Claude Code sends notifications | No |
|
|
| **UserPromptSubmit** | When users submit prompts, before Claude processes | No |
|
|
| **Stop** | When main Claude agent finishes responding | No |
|
|
| **SubagentStop** | When subagents (Task tool calls) complete | No |
|
|
| **PreCompact** | Before compacting operations | No |
|
|
| **SessionStart** | When sessions start or resume | No |
|
|
| **SessionEnd** | When sessions terminate | No |
|
|
|
|
## Hook Types (Official Specification)
|
|
|
|
**Command Hooks** (`type: "command"`):
|
|
|
|
- Execute bash scripts with file system access
|
|
- Default timeout: 60 seconds (customizable per hook)
|
|
- Fast, deterministic operations
|
|
- Use for formatters, linters, file ops, git commands
|
|
|
|
**Prompt-Based Hooks** (`type: "prompt"`):
|
|
|
|
- Send queries to Claude Haiku for context-aware decisions
|
|
- Available for: `Stop`, `SubagentStop`, `UserPromptSubmit`, `PreToolUse`
|
|
- Use when judgment/context understanding needed
|
|
- Natural language evaluation
|
|
|
|
**Rule of thumb:** If you can write it as a bash script = command hook. If you need judgment = prompt hook.
|
|
|
|
## Hook Script Implementation Patterns (Best Practices)
|
|
|
|
### Bash vs Python for Hook Scripts
|
|
|
|
**Bash is ideal for:**
|
|
|
|
- Simple file operations (formatting, linting with external tools)
|
|
- Calling CLI tools directly
|
|
- Quick text processing with standard utilities
|
|
- Minimal logic, mostly orchestration
|
|
|
|
**Python is better for:**
|
|
|
|
- Complex validation logic
|
|
- JSON parsing and manipulation
|
|
- Advanced text processing
|
|
- Using Python libraries for analysis
|
|
- Multi-step processing with error handling
|
|
|
|
### Python Hook Scripts with UV (Best Practice)
|
|
|
|
For Python-based hooks requiring dependencies or complex logic, use UV's single-file script format with inline metadata. This provides self-contained, executable scripts without separate environment setup.
|
|
|
|
**When to use Python hooks:**
|
|
|
|
- Parsing complex JSON from stdin
|
|
- Advanced validation requiring libraries (AST analysis, schema validation)
|
|
- Multi-step processing beyond simple shell pipelines
|
|
- Need for structured error handling and reporting
|
|
|
|
**Pattern:** Use Skill tool: skill="box-factory:uv-scripts"
|
|
|
|
The uv-scripts skill provides complete patterns for creating Python hook scripts with inline dependencies, proper shebangs, and Claude Code integration.
|
|
|
|
**Quick example:**
|
|
|
|
```python
|
|
#!/usr/bin/env -S uv run --script
|
|
# /// script
|
|
# dependencies = ["ruff"]
|
|
# ///
|
|
|
|
import subprocess
|
|
import sys
|
|
import os
|
|
|
|
def main():
|
|
file_paths = os.environ.get("CLAUDE_FILE_PATHS", "").split()
|
|
if not file_paths:
|
|
sys.exit(0)
|
|
|
|
result = subprocess.run(["ruff", "check", *file_paths])
|
|
sys.exit(result.returncode)
|
|
|
|
if __name__ == "__main__":
|
|
main()
|
|
```
|
|
|
|
**Key advantages:**
|
|
|
|
- Self-contained (dependencies declared inline)
|
|
- No separate virtual environment management
|
|
- Executable directly with proper shebang
|
|
- Fast startup with UV's performance
|
|
- Perfect for plugin hooks (ships with dependencies)
|
|
|
|
## Matcher Syntax (Official Specification)
|
|
|
|
Matchers specify which tools trigger hooks (applies to PreToolUse and PostToolUse only):
|
|
|
|
**Exact matching:**
|
|
```json
|
|
"matcher": "Write"
|
|
```
|
|
|
|
**Regex patterns with pipe:**
|
|
```json
|
|
"matcher": "Edit|Write"
|
|
"matcher": "Notebook.*"
|
|
```
|
|
|
|
**Wildcard (match all):**
|
|
```json
|
|
"matcher": "*"
|
|
```
|
|
|
|
**Empty matcher:**
|
|
Omit for events like `UserPromptSubmit` that don't apply to specific tools.
|
|
|
|
**Note:** Matchers are case-sensitive.
|
|
|
|
## Configuration Structure (Official Specification)
|
|
|
|
Located in `~/.claude/settings.json`, `.claude/settings.json`, or `.claude/settings.local.json`:
|
|
|
|
```json
|
|
{
|
|
"hooks": {
|
|
"EventName": [
|
|
{
|
|
"matcher": "ToolPattern",
|
|
"hooks": [
|
|
{
|
|
"type": "command",
|
|
"command": "bash-command",
|
|
"timeout": 30
|
|
}
|
|
]
|
|
}
|
|
]
|
|
}
|
|
}
|
|
```
|
|
|
|
**Timeout field:** Optional, specified in seconds (default 60).
|
|
|
|
## Hook Input (stdin) - Official Specification
|
|
|
|
All hooks receive JSON via stdin:
|
|
|
|
**Base structure (all events):**
|
|
```json
|
|
{
|
|
"session_id": "string",
|
|
"transcript_path": "path/to/transcript.jsonl",
|
|
"cwd": "current/working/directory",
|
|
"permission_mode": "default|plan|acceptEdits|bypassPermissions",
|
|
"hook_event_name": "EventName"
|
|
}
|
|
```
|
|
|
|
**Event-specific fields:**
|
|
|
|
- **PreToolUse/PostToolUse**: Adds `tool_name`, `tool_input`
|
|
- **UserPromptSubmit**: Adds `prompt`
|
|
- Other events may include additional context
|
|
|
|
**Best practice:** Parse stdin JSON to access context, don't rely only on environment variables.
|
|
|
|
## Hook Output (stdout) - Official Specification
|
|
|
|
Two approaches for returning results:
|
|
|
|
### Simple Exit Code Approach
|
|
|
|
Just use exit codes and stderr for errors. Most common for straightforward hooks.
|
|
|
|
### Advanced JSON Output
|
|
|
|
Return structured JSON for sophisticated control:
|
|
|
|
```json
|
|
{
|
|
"continue": true,
|
|
"stopReason": "Custom message",
|
|
"suppressOutput": true,
|
|
"systemMessage": "Warning to display",
|
|
"hookSpecificOutput": {
|
|
"hookEventName": "EventName",
|
|
"additionalContext": "string"
|
|
}
|
|
}
|
|
```
|
|
|
|
### PostToolUse Communication Pattern (CRITICAL)
|
|
|
|
**Key insight:** PostToolUse hooks have two output channels with different visibility:
|
|
|
|
**For messages visible DIRECTLY to users (no verbose mode required):**
|
|
|
|
Use `systemMessage` field - displays immediately to users:
|
|
|
|
```json
|
|
{
|
|
"systemMessage": "Markdown formatted: path/to/file.md"
|
|
}
|
|
```
|
|
|
|
**For messages visible ONLY to Claude (user must enable verbose mode):**
|
|
|
|
Use `additionalContext` in `hookSpecificOutput`:
|
|
|
|
```json
|
|
{
|
|
"hookSpecificOutput": {
|
|
"hookEventName": "PostToolUse",
|
|
"additionalContext": "Internal context for Claude's awareness"
|
|
}
|
|
}
|
|
```
|
|
|
|
**Complete output pattern:**
|
|
|
|
```python
|
|
import json
|
|
output = {
|
|
"systemMessage": "Formatted successfully: file.md", # Shows to user directly
|
|
"hookSpecificOutput": {
|
|
"hookEventName": "PostToolUse",
|
|
"additionalContext": "Additional context for Claude" # Only in verbose mode
|
|
}
|
|
}
|
|
print(json.dumps(output), flush=True)
|
|
sys.exit(0)
|
|
```
|
|
|
|
**Common mistake:** Using only `additionalContext` when user feedback is needed. This requires users to enable verbose mode (CTRL-O) to see output.
|
|
|
|
**Correct pattern:**
|
|
- **User feedback needed:** Use `systemMessage` (visible immediately)
|
|
- **Claude context only:** Use `additionalContext` (verbose mode only)
|
|
- **Both:** Include both fields in the JSON output
|
|
- **Blocking errors:** Use exit 2 with stderr (rare, security/safety only)
|
|
|
|
### PreToolUse Special Output
|
|
|
|
For modifying or blocking tool execution:
|
|
|
|
```json
|
|
{
|
|
"permissionDecision": "allow|deny|ask",
|
|
"updatedInput": {
|
|
"modified": "tool parameters"
|
|
}
|
|
}
|
|
```
|
|
|
|
**Use case:** Modify tool inputs before execution (e.g., add safety flags to bash commands).
|
|
|
|
## Environment Variables (Official Specification)
|
|
|
|
Available in command hooks:
|
|
|
|
| Variable | Purpose |
|
|
|----------|---------|
|
|
| `$CLAUDE_PROJECT_DIR` | Absolute path to project root |
|
|
| `$CLAUDE_ENV_FILE` | File path for persisting env vars (SessionStart only) |
|
|
| `${CLAUDE_PLUGIN_ROOT}` | Plugin directory path (for plugin hooks) |
|
|
| `$CLAUDE_CODE_REMOTE` | `"true"` for remote, empty for local execution |
|
|
|
|
**Best practice:** Always quote variables: `"$CLAUDE_PROJECT_DIR"` not `$CLAUDE_PROJECT_DIR`
|
|
|
|
## Decision Framework
|
|
|
|
### Hook vs Agent vs Command
|
|
|
|
**Use Hook when:**
|
|
|
|
- Need guaranteed execution every time
|
|
- Simple, deterministic rule (format, lint, validate)
|
|
- Integrating with external tools
|
|
- Performance/safety enforcement
|
|
- Must happen at specific lifecycle event
|
|
|
|
**Use Agent when:**
|
|
|
|
- Complex decision-making involved
|
|
- Need Claude's intelligence for analysis
|
|
- Context-dependent logic
|
|
- Natural language processing needed
|
|
- Can be triggered proactively by context
|
|
|
|
**Use Command (Slash Command) when:**
|
|
|
|
- User wants explicit control over when it runs
|
|
- Not tied to lifecycle events
|
|
- One-off operations
|
|
|
|
## Common Use Patterns (Best Practices)
|
|
|
|
### SessionStart Pattern
|
|
|
|
**Purpose:** Initialize session state, inject context
|
|
|
|
```json
|
|
{
|
|
"SessionStart": [
|
|
{
|
|
"hooks": [
|
|
{
|
|
"type": "command",
|
|
"command": "cat .claude/project-context.md"
|
|
}
|
|
]
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
**Key:** stdout becomes Claude's context. Use to load project guidelines, conventions, or state.
|
|
|
|
### UserPromptSubmit Pattern
|
|
|
|
**Purpose:** Validate or enrich prompts before Claude sees them
|
|
|
|
```json
|
|
{
|
|
"UserPromptSubmit": [
|
|
{
|
|
"hooks": [
|
|
{
|
|
"type": "command",
|
|
"command": "~/.claude/hooks/inject-security-reminders.sh"
|
|
}
|
|
]
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
**Key:** stdout goes to Claude. Can add context or use exit 2 to block prompts.
|
|
|
|
### PreToolUse Pattern
|
|
|
|
**Purpose:** Validate or modify before execution
|
|
|
|
```json
|
|
{
|
|
"PreToolUse": [
|
|
{
|
|
"matcher": "Bash",
|
|
"hooks": [
|
|
{
|
|
"type": "command",
|
|
"command": "~/.claude/hooks/security-check.sh"
|
|
}
|
|
]
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
**Power move:** Exit 2 to block dangerous commands and explain why to Claude.
|
|
|
|
### PostToolUse Pattern
|
|
|
|
**Purpose:** React to successful tool completion
|
|
|
|
```json
|
|
{
|
|
"PostToolUse": [
|
|
{
|
|
"matcher": "Write|Edit",
|
|
"hooks": [
|
|
{
|
|
"type": "command",
|
|
"command": "prettier --write \"$CLAUDE_FILE_PATHS\" 2>/dev/null || true"
|
|
}
|
|
]
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
**Common uses:** Format code, run linters, update documentation, cleanup.
|
|
|
|
**CRITICAL for PostToolUse:** To communicate results to users, hooks must output JSON to stdout with `systemMessage`:
|
|
|
|
```python
|
|
#!/usr/bin/env -S uv run --quiet --script
|
|
# /// script
|
|
# dependencies = []
|
|
# ///
|
|
import json
|
|
import sys
|
|
|
|
def output_json_response(system_message=None, additional_context=None):
|
|
"""Output JSON response for Claude to process."""
|
|
response = {}
|
|
if system_message:
|
|
response["systemMessage"] = system_message # Visible directly to user
|
|
if additional_context:
|
|
response["hookSpecificOutput"] = {
|
|
"hookEventName": "PostToolUse",
|
|
"additionalContext": additional_context # Only visible in verbose mode
|
|
}
|
|
print(json.dumps(response), flush=True)
|
|
|
|
# Read hook input from stdin
|
|
hook_input = json.load(sys.stdin)
|
|
file_path = hook_input.get("tool_input", {}).get("file_path")
|
|
|
|
# Run linter/formatter
|
|
# ...
|
|
|
|
# Communicate result directly to user
|
|
output_json_response(system_message=f"Formatted successfully: {file_path}")
|
|
sys.exit(0)
|
|
```
|
|
|
|
**Common mistakes:**
|
|
- Using only `additionalContext` when user feedback is needed (requires verbose mode)
|
|
- Writing to stderr instead of JSON stdout (completely invisible)
|
|
|
|
### Stop Pattern
|
|
|
|
**Purpose:** Session cleanup, final actions
|
|
|
|
```json
|
|
{
|
|
"Stop": [
|
|
{
|
|
"hooks": [
|
|
{
|
|
"type": "command",
|
|
"command": "~/.claude/hooks/auto-commit.sh"
|
|
}
|
|
]
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
**Note:** Claude already responded, can't change that. Use for cleanup, notifications, test runs.
|
|
|
|
## Common Pitfalls (Best Practices)
|
|
|
|
### Pitfall #1: Blocking Everything
|
|
|
|
**Problem:** Overly aggressive hook blocks all operations
|
|
|
|
```json
|
|
{
|
|
"PreToolUse": [
|
|
{
|
|
"matcher": "*",
|
|
"hooks": [{"type": "command", "command": "exit 2"}]
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
**Result:** Claude can't do anything, unusable.
|
|
|
|
**Better:** Selective blocking with clear criteria for security/safety only.
|
|
|
|
### Pitfall #2: Slow Hooks
|
|
|
|
**Problem:** Hook takes 30+ seconds, blocks user experience
|
|
|
|
```bash
|
|
npm install # Slow, blocking
|
|
exit 0
|
|
```
|
|
|
|
**Impact:** Claude waits, terrible UX.
|
|
|
|
**Better:** Fast validation or background execution
|
|
|
|
```bash
|
|
npm outdated | head -5 # Quick check
|
|
exit 0
|
|
```
|
|
|
|
**Or:** Adjust timeout for legitimately long operations:
|
|
|
|
```json
|
|
{
|
|
"type": "command",
|
|
"command": "long-running-task.sh",
|
|
"timeout": 120
|
|
}
|
|
```
|
|
|
|
### Pitfall #3: Silent Failures
|
|
|
|
**Problem:** Errors disappear into the void
|
|
|
|
```bash
|
|
important-check || true
|
|
exit 0
|
|
```
|
|
|
|
**Result:** User never knows check failed.
|
|
|
|
**Better:** Clear error communication
|
|
|
|
```bash
|
|
if ! important-check; then
|
|
echo "Check failed: [specific reason]" >&2
|
|
exit 1 # Non-blocking, but visible
|
|
fi
|
|
exit 0
|
|
```
|
|
|
|
### Pitfall #4: Assuming User Interaction
|
|
|
|
**Problem:** Hook expects user input
|
|
|
|
```bash
|
|
read -p "Confirm? " response
|
|
exit 0
|
|
```
|
|
|
|
**Result:** Hook hangs indefinitely (no user to respond).
|
|
|
|
**Better:** Fully automated decisions based on stdin JSON or environment.
|
|
|
|
### Pitfall #5: Ignoring Security
|
|
|
|
**Problem:** Hook doesn't validate inputs, vulnerable to path traversal
|
|
|
|
```bash
|
|
cat "$SOME_PATH" # Dangerous if not validated
|
|
```
|
|
|
|
**Result:** Could access sensitive files outside project.
|
|
|
|
**Better:** Validate and sanitize
|
|
|
|
```bash
|
|
if [[ "$SOME_PATH" == *".."* ]]; then
|
|
echo "Path traversal detected" >&2
|
|
exit 2
|
|
fi
|
|
# Continue safely
|
|
```
|
|
|
|
**Official guidance:** Skip sensitive files (`.env`, `.git/`, credentials). Validate inputs from stdin.
|
|
|
|
### Pitfall #6: Not Quoting Variables
|
|
|
|
**Problem:** Unquoted shell variables break with spaces
|
|
|
|
```bash
|
|
prettier --write $CLAUDE_FILE_PATHS # Breaks if path has spaces
|
|
```
|
|
|
|
**Better:** Always quote variables
|
|
|
|
```bash
|
|
prettier --write "$CLAUDE_FILE_PATHS"
|
|
```
|
|
|
|
### Pitfall #7: PostToolUse Using Wrong Output Field
|
|
|
|
**Problem:** Hook uses `additionalContext` when user feedback is needed
|
|
|
|
```python
|
|
# Wrong - Only visible in verbose mode (CTRL-O)
|
|
import json
|
|
output = {
|
|
"hookSpecificOutput": {
|
|
"hookEventName": "PostToolUse",
|
|
"additionalContext": "Formatted successfully: file.md"
|
|
}
|
|
}
|
|
print(json.dumps(output), flush=True)
|
|
sys.exit(0)
|
|
```
|
|
|
|
**Result:** User must enable verbose mode to see feedback.
|
|
|
|
**Better:** Use `systemMessage` for direct user visibility
|
|
|
|
```python
|
|
# Correct - Visible immediately to user
|
|
import json
|
|
output = {
|
|
"systemMessage": "Formatted successfully: file.md"
|
|
}
|
|
print(json.dumps(output), flush=True)
|
|
sys.exit(0)
|
|
```
|
|
|
|
**Why:**
|
|
- `systemMessage` displays directly to users (no verbose mode required)
|
|
- `additionalContext` only visible in verbose mode (CTRL-O) or as Claude's context
|
|
- stderr output is only for blocking errors (exit 2)
|
|
|
|
## Security Considerations (Official Guidance)
|
|
|
|
**Critical warning from docs:** "Claude Code hooks execute arbitrary shell commands on your system automatically."
|
|
|
|
**Implications:**
|
|
|
|
- Hooks can access/modify any files your user account permits
|
|
- You bear sole responsibility for configured commands
|
|
- Test thoroughly in safe environments first
|
|
- Review hooks from untrusted sources carefully
|
|
|
|
**Protection mechanism:**
|
|
|
|
- Configuration snapshots captured at startup
|
|
- Hook changes require review via `/hooks` menu
|
|
- Prevents mid-session malicious modifications
|
|
|
|
**Best practices:**
|
|
|
|
1. Validate and sanitize all inputs from stdin
|
|
2. Block path traversal (`..` in paths)
|
|
3. Use absolute paths with `$CLAUDE_PROJECT_DIR`
|
|
4. Skip sensitive files (`.env`, credentials, `.git/`)
|
|
5. For prompt-based hooks, be specific about criteria
|
|
6. Set appropriate timeouts
|
|
7. Test in isolated environments first
|
|
|
|
## Debugging Hooks (Best Practices)
|
|
|
|
**View hook execution:**
|
|
|
|
Press **CTRL-R** in Claude Code to see:
|
|
- Hook stdout/stderr
|
|
- Execution flow
|
|
- Exit codes
|
|
- Timing information
|
|
|
|
**Add logging to hooks:**
|
|
|
|
```bash
|
|
echo "Hook triggered: $(date)" >> ~/.claude/hook-log.txt
|
|
echo "Input: $SOME_VAR" >> ~/.claude/hook-log.txt
|
|
# Continue with hook logic
|
|
exit 0
|
|
```
|
|
|
|
**Parse stdin for debugging:**
|
|
|
|
```bash
|
|
# Save stdin to debug
|
|
cat > /tmp/hook-debug.json
|
|
cat /tmp/hook-debug.json | jq '.' # Pretty print
|
|
exit 0
|
|
```
|
|
|
|
## Advanced Features (Official Specification)
|
|
|
|
### Modifying Tool Inputs (PreToolUse)
|
|
|
|
Return JSON to modify tool parameters:
|
|
|
|
```bash
|
|
#!/bin/bash
|
|
# Read stdin
|
|
INPUT=$(cat)
|
|
|
|
# Add safety flag to bash commands
|
|
MODIFIED=$(echo "$INPUT" | jq '.tool_input.command = .tool_input.command + " --safe-mode"')
|
|
|
|
# Return modified input
|
|
echo "$MODIFIED" | jq '{permissionDecision: "allow", updatedInput: .tool_input}'
|
|
exit 0
|
|
```
|
|
|
|
### Custom Timeouts
|
|
|
|
Adjust timeout per hook:
|
|
|
|
```json
|
|
{
|
|
"PostToolUse": [
|
|
{
|
|
"matcher": "Write",
|
|
"hooks": [
|
|
{
|
|
"type": "command",
|
|
"command": "./long-build.sh",
|
|
"timeout": 300
|
|
}
|
|
]
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
### Prompt-Based Intelligence
|
|
|
|
Use Claude Haiku for context-aware decisions:
|
|
|
|
```json
|
|
{
|
|
"PreToolUse": [
|
|
{
|
|
"matcher": "Bash",
|
|
"hooks": [
|
|
{
|
|
"type": "prompt",
|
|
"command": "Analyze this bash command for security risks. If dangerous, explain why and recommend safer alternative."
|
|
}
|
|
]
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
## Plugin Integration
|
|
|
|
When creating hooks for plugins:
|
|
|
|
**Structure:**
|
|
```
|
|
my-plugin/
|
|
├── .claude-plugin/plugin.json
|
|
└── hooks/
|
|
└── hooks.json
|
|
```
|
|
|
|
**Reference plugin root:**
|
|
```bash
|
|
"${CLAUDE_PLUGIN_ROOT}/scripts/hook-script.sh"
|
|
```
|
|
|
|
See plugin-design skill for complete plugin context.
|
|
|
|
## Hook Quality Checklist
|
|
|
|
Before deploying hooks:
|
|
|
|
**Functionality (from official docs):**
|
|
|
|
- ✓ Correct event type for use case
|
|
- ✓ Valid matcher pattern (if applicable)
|
|
- ✓ Proper JSON structure in settings
|
|
- ✓ Appropriate timeout configured
|
|
|
|
**Quality (best practices):**
|
|
|
|
- ✓ Fast execution (< 60s or custom timeout)
|
|
- ✓ Clear error messages to stderr
|
|
- ✓ Appropriate exit codes (0, 2, other)
|
|
- ✓ No user interaction required
|
|
- ✓ Variables quoted properly
|
|
- ✓ Inputs validated/sanitized
|
|
|
|
**Security (best practices):**
|
|
|
|
- ✓ Path traversal blocked
|
|
- ✓ Sensitive files skipped
|
|
- ✓ Absolute paths used
|
|
- ✓ No secret exposure
|
|
- ✓ Tested in safe environment
|
|
|
|
## Example: High-Quality Hook
|
|
|
|
**Basic (hypothetical docs example):**
|
|
|
|
```json
|
|
{
|
|
"PostToolUse": [
|
|
{
|
|
"matcher": "Write",
|
|
"hooks": [{"type": "command", "command": "prettier --write"}]
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
**Issues:**
|
|
|
|
- ❌ Missing file path variable
|
|
- ❌ No error handling
|
|
- ❌ Doesn't catch Edit operations
|
|
|
|
**Excellent (applying best practices):**
|
|
|
|
```json
|
|
{
|
|
"PostToolUse": [
|
|
{
|
|
"matcher": "Write|Edit",
|
|
"hooks": [
|
|
{
|
|
"type": "command",
|
|
"command": "prettier --write \"$CLAUDE_FILE_PATHS\" 2>/dev/null || true",
|
|
"timeout": 30
|
|
}
|
|
]
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
**Improvements:**
|
|
|
|
- ✅ Uses `$CLAUDE_FILE_PATHS` variable
|
|
- ✅ Quoted variable for spaces
|
|
- ✅ Error suppression (|| true) prevents blocking
|
|
- ✅ Catches both Write and Edit
|
|
- ✅ Custom timeout for faster failures
|
|
- ✅ Redirects stderr to avoid noise
|
|
|
|
## Documentation References
|
|
|
|
These are the authoritative sources. Fetch them before creating hooks:
|
|
|
|
**Core specifications:**
|
|
|
|
- https://code.claude.com/docs/en/hooks - Complete hook reference
|
|
|
|
**Related topics:**
|
|
|
|
- See agent-design skill for when to use agents instead
|
|
- See slash-command-design skill for user-triggered operations
|
|
- See plugin-design skill for packaging hooks in plugins
|
|
- See uv-scripts skill for Python-based hook implementation patterns
|
|
|
|
**Remember:** Official docs provide structure and features. This skill provides best practices and patterns for creating excellent hooks.
|