Transform repository into a plugin marketplace structure with two plugins: - claude-code plugin: Complete toolkit with 5 skills * claude-code-plugins * claude-code-slash-commands * claude-code-hooks * claude-code-subagents * claude-code-memory - claude-skills plugin: Meta-skill for creating Agent Skills * Comprehensive best practices guide * Templates and examples * Progressive disclosure patterns Infrastructure: - Add marketplace.json manifest - Create plugin.json for each plugin - Update documentation for marketplace structure - Add contribution and testing guides Installation: - /plugin install claude-code@claude-skills - /plugin install claude-skills@claude-skills 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1050 lines
18 KiB
Markdown
1050 lines
18 KiB
Markdown
---
|
|
name: Claude Code Hooks
|
|
description: Configure event-driven hooks for Claude Code to automate workflows, validate code, inject context, and control tool execution. Use when setting up automation, enforcing standards, or integrating external tools with Claude Code.
|
|
---
|
|
|
|
# Claude Code Hooks
|
|
|
|
## When to Use This Skill
|
|
|
|
Use this skill when:
|
|
- Automating workflows with event-driven triggers
|
|
- Enforcing code standards or security policies
|
|
- Validating changes before/after tool execution
|
|
- Injecting context at session start
|
|
- Logging or monitoring tool usage
|
|
- Controlling MCP tool permissions
|
|
- Setting up team-wide automation
|
|
|
|
Do NOT use this skill for:
|
|
- Creating slash commands (use claude-code-slash-commands skill)
|
|
- Building full plugins (use claude-code-plugins skill)
|
|
- One-time scripts (just run them directly)
|
|
|
|
## Quick Start: Creating a Hook
|
|
|
|
### 1. Basic Hook Configuration
|
|
|
|
Create or edit settings file:
|
|
|
|
```bash
|
|
# Project-wide (team shared)
|
|
.claude/settings.json
|
|
|
|
# User-specific (not shared)
|
|
.claude/settings.local.json
|
|
|
|
# Global (all projects)
|
|
~/.claude/settings.json
|
|
```
|
|
|
|
### 2. Simple Example - Log File Changes
|
|
|
|
```json
|
|
{
|
|
"hooks": {
|
|
"PostToolUse": [
|
|
{
|
|
"matcher": "Write|Edit",
|
|
"hooks": [
|
|
{
|
|
"type": "command",
|
|
"command": "echo \"$(date): Modified $TOOL_ARGS\" >> .claude/changes.log"
|
|
}
|
|
]
|
|
}
|
|
]
|
|
}
|
|
}
|
|
```
|
|
|
|
**Result**: Every file write/edit gets logged with timestamp.
|
|
|
|
## Hook Types
|
|
|
|
### SessionStart
|
|
|
|
Runs when a session begins (before any interaction):
|
|
|
|
```json
|
|
{
|
|
"hooks": {
|
|
"SessionStart": [
|
|
{
|
|
"matcher": "*",
|
|
"hooks": [
|
|
{
|
|
"type": "command",
|
|
"command": "echo 'Project: MyApp | Env: production' > .claude/context.txt"
|
|
}
|
|
]
|
|
}
|
|
]
|
|
}
|
|
}
|
|
```
|
|
|
|
**Use For**: Injecting project context, loading environment info
|
|
|
|
### SessionEnd
|
|
|
|
Runs when a session ends:
|
|
|
|
```json
|
|
{
|
|
"hooks": {
|
|
"SessionEnd": [
|
|
{
|
|
"matcher": "*",
|
|
"hooks": [
|
|
{
|
|
"type": "command",
|
|
"command": "git add . && git stash save 'Claude session backup'"
|
|
}
|
|
]
|
|
}
|
|
]
|
|
}
|
|
}
|
|
```
|
|
|
|
**Use For**: Cleanup, backups, state preservation
|
|
|
|
### PreToolUse
|
|
|
|
Runs after Claude creates tool parameters but before execution:
|
|
|
|
```json
|
|
{
|
|
"hooks": {
|
|
"PreToolUse": [
|
|
{
|
|
"matcher": "Write",
|
|
"hooks": [
|
|
{
|
|
"type": "command",
|
|
"command": "echo 'About to write: $TOOL_ARGS' && exit 0"
|
|
}
|
|
]
|
|
}
|
|
]
|
|
}
|
|
}
|
|
```
|
|
|
|
**Use For**: Validation, permission checks, preprocessing
|
|
|
|
### PostToolUse
|
|
|
|
Runs immediately after tool completes successfully:
|
|
|
|
```json
|
|
{
|
|
"hooks": {
|
|
"PostToolUse": [
|
|
{
|
|
"matcher": "Write|Edit",
|
|
"hooks": [
|
|
{
|
|
"type": "command",
|
|
"command": "npx prettier --write $TOOL_ARGS"
|
|
}
|
|
]
|
|
}
|
|
]
|
|
}
|
|
}
|
|
```
|
|
|
|
**Use For**: Formatting, linting, post-processing
|
|
|
|
### UserPromptSubmit
|
|
|
|
Runs when user submits a prompt (before Claude processes it):
|
|
|
|
```json
|
|
{
|
|
"hooks": {
|
|
"UserPromptSubmit": [
|
|
{
|
|
"matcher": "*",
|
|
"hooks": [
|
|
{
|
|
"type": "command",
|
|
"command": "date >> .claude/prompt-log.txt && exit 0"
|
|
}
|
|
]
|
|
}
|
|
]
|
|
}
|
|
}
|
|
```
|
|
|
|
**Use For**: Logging, analytics, context injection
|
|
|
|
### Notification
|
|
|
|
Runs when Claude sends notifications (permission requests, idle waiting):
|
|
|
|
```json
|
|
{
|
|
"hooks": {
|
|
"Notification": [
|
|
{
|
|
"matcher": "*",
|
|
"hooks": [
|
|
{
|
|
"type": "command",
|
|
"command": "osascript -e 'display notification \"Claude needs attention\" with title \"Claude Code\"'"
|
|
}
|
|
]
|
|
}
|
|
]
|
|
}
|
|
}
|
|
```
|
|
|
|
**Use For**: Desktop notifications, alerts
|
|
|
|
### Stop / SubagentStop
|
|
|
|
Runs when agents finish responding:
|
|
|
|
```json
|
|
{
|
|
"hooks": {
|
|
"Stop": [
|
|
{
|
|
"matcher": "*",
|
|
"hooks": [
|
|
{
|
|
"type": "command",
|
|
"command": "echo 'Response complete' >> .claude/activity.log && exit 0"
|
|
}
|
|
]
|
|
}
|
|
]
|
|
}
|
|
}
|
|
```
|
|
|
|
**Use For**: Post-response processing, logging
|
|
|
|
### PreCompact
|
|
|
|
Runs before context compaction:
|
|
|
|
```json
|
|
{
|
|
"hooks": {
|
|
"PreCompact": [
|
|
{
|
|
"matcher": "*",
|
|
"hooks": [
|
|
{
|
|
"type": "command",
|
|
"command": "echo 'Compacting context...' && exit 0"
|
|
}
|
|
]
|
|
}
|
|
]
|
|
}
|
|
}
|
|
```
|
|
|
|
**Use For**: Logging, saving state before compaction
|
|
|
|
## Matcher Patterns
|
|
|
|
### Tool Name Matching
|
|
|
|
```json
|
|
// Single tool
|
|
"matcher": "Write"
|
|
|
|
// Multiple tools (OR)
|
|
"matcher": "Write|Edit|Read"
|
|
|
|
// All tools
|
|
"matcher": "*"
|
|
|
|
// Regex patterns supported
|
|
"matcher": "Bash.*|Git.*"
|
|
```
|
|
|
|
### Common Patterns
|
|
|
|
```json
|
|
// File operations
|
|
"matcher": "Write|Edit|Delete"
|
|
|
|
// Read operations
|
|
"matcher": "Read|Grep|Glob"
|
|
|
|
// Git operations
|
|
"matcher": "Bash(git:*)"
|
|
|
|
// All Bash commands
|
|
"matcher": "Bash"
|
|
```
|
|
|
|
## Hook Configuration
|
|
|
|
### Basic Structure
|
|
|
|
```json
|
|
{
|
|
"type": "command",
|
|
"command": "your-command-here",
|
|
"timeout": 60
|
|
}
|
|
```
|
|
|
|
### Command Field
|
|
|
|
Use bash commands with environment variables:
|
|
|
|
```json
|
|
{
|
|
"type": "command",
|
|
"command": "cd $CLAUDE_PROJECT_DIR && npm test"
|
|
}
|
|
```
|
|
|
|
### Timeout Field
|
|
|
|
Optional execution limit in seconds (default: 60):
|
|
|
|
```json
|
|
{
|
|
"type": "command",
|
|
"command": "long-running-task",
|
|
"timeout": 300 // 5 minutes
|
|
}
|
|
```
|
|
|
|
## Exit Codes & Control Flow
|
|
|
|
### Exit Code 0 - Success (Continue)
|
|
|
|
```bash
|
|
# Hook succeeds, stdout shown to user (most events)
|
|
echo "Success message"
|
|
exit 0
|
|
```
|
|
|
|
### Exit Code 2 - Blocking Error
|
|
|
|
```bash
|
|
# Hook blocks operation, stderr sent to Claude
|
|
echo "Error: Cannot proceed" >&2
|
|
exit 2
|
|
```
|
|
|
|
**Use For**: Validation failures, permission denials
|
|
|
|
### Other Exit Codes - Non-Blocking Error
|
|
|
|
```bash
|
|
# Hook fails but operation continues
|
|
echo "Warning: Hook failed" >&2
|
|
exit 1
|
|
```
|
|
|
|
## Environment Variables
|
|
|
|
### Available Variables
|
|
|
|
```bash
|
|
$CLAUDE_PROJECT_DIR # Current project directory
|
|
$TOOL_NAME # Name of the tool being used
|
|
$TOOL_ARGS # Arguments passed to the tool
|
|
$HOOK_EVENT # Event type (PreToolUse, PostToolUse, etc.)
|
|
```
|
|
|
|
### Example Usage
|
|
|
|
```json
|
|
{
|
|
"hooks": {
|
|
"PostToolUse": [
|
|
{
|
|
"matcher": "Write",
|
|
"hooks": [
|
|
{
|
|
"type": "command",
|
|
"command": "cd $CLAUDE_PROJECT_DIR && git add $TOOL_ARGS"
|
|
}
|
|
]
|
|
}
|
|
]
|
|
}
|
|
}
|
|
```
|
|
|
|
## Complete Examples
|
|
|
|
### 1. Auto-Format on Save
|
|
|
|
Format files automatically after writing/editing:
|
|
|
|
```json
|
|
{
|
|
"hooks": {
|
|
"PostToolUse": [
|
|
{
|
|
"matcher": "Write|Edit",
|
|
"hooks": [
|
|
{
|
|
"type": "command",
|
|
"command": "cd $CLAUDE_PROJECT_DIR && npx prettier --write $TOOL_ARGS && exit 0"
|
|
}
|
|
]
|
|
}
|
|
]
|
|
}
|
|
}
|
|
```
|
|
|
|
### 2. Security Validation
|
|
|
|
Block writes containing secrets:
|
|
|
|
```json
|
|
{
|
|
"hooks": {
|
|
"PreToolUse": [
|
|
{
|
|
"matcher": "Write|Edit",
|
|
"hooks": [
|
|
{
|
|
"type": "command",
|
|
"command": "if grep -qiE '(password|api[_-]?key|secret)' $TOOL_ARGS 2>/dev/null; then echo 'Error: Possible secret detected' >&2; exit 2; fi; exit 0",
|
|
"timeout": 30
|
|
}
|
|
]
|
|
}
|
|
]
|
|
}
|
|
}
|
|
```
|
|
|
|
### 3. Auto-Git on Changes
|
|
|
|
Automatically stage and commit changes:
|
|
|
|
```json
|
|
{
|
|
"hooks": {
|
|
"PostToolUse": [
|
|
{
|
|
"matcher": "Write|Edit",
|
|
"hooks": [
|
|
{
|
|
"type": "command",
|
|
"command": "cd $CLAUDE_PROJECT_DIR && git add $TOOL_ARGS && git commit -m 'Auto-commit: Modified $TOOL_ARGS' && exit 0"
|
|
}
|
|
]
|
|
}
|
|
]
|
|
}
|
|
}
|
|
```
|
|
|
|
### 4. Inject Project Context
|
|
|
|
Load project info at session start:
|
|
|
|
```json
|
|
{
|
|
"hooks": {
|
|
"SessionStart": [
|
|
{
|
|
"matcher": "*",
|
|
"hooks": [
|
|
{
|
|
"type": "command",
|
|
"command": "cat << EOF\nProject: MyApp\nEnvironment: Production\nKey Files: src/config.js, .env.example\nCoding Standards: See CONTRIBUTING.md\nEOF"
|
|
}
|
|
]
|
|
}
|
|
]
|
|
}
|
|
}
|
|
```
|
|
|
|
### 5. Test Before Commit
|
|
|
|
Run tests before allowing file writes:
|
|
|
|
```json
|
|
{
|
|
"hooks": {
|
|
"PreToolUse": [
|
|
{
|
|
"matcher": "Write|Edit",
|
|
"hooks": [
|
|
{
|
|
"type": "command",
|
|
"command": "cd $CLAUDE_PROJECT_DIR && npm test -- --silent || (echo 'Tests failed, cannot save' >&2; exit 2)",
|
|
"timeout": 120
|
|
}
|
|
]
|
|
}
|
|
]
|
|
}
|
|
}
|
|
```
|
|
|
|
### 6. Log All Activity
|
|
|
|
Track all tool usage:
|
|
|
|
```json
|
|
{
|
|
"hooks": {
|
|
"PreToolUse": [
|
|
{
|
|
"matcher": "*",
|
|
"hooks": [
|
|
{
|
|
"type": "command",
|
|
"command": "echo \"$(date '+%Y-%m-%d %H:%M:%S') - $TOOL_NAME - $TOOL_ARGS\" >> $CLAUDE_PROJECT_DIR/.claude/activity.log && exit 0"
|
|
}
|
|
]
|
|
}
|
|
]
|
|
}
|
|
}
|
|
```
|
|
|
|
### 7. Enforce File Permissions
|
|
|
|
Ensure proper permissions on sensitive files:
|
|
|
|
```json
|
|
{
|
|
"hooks": {
|
|
"PostToolUse": [
|
|
{
|
|
"matcher": "Write",
|
|
"hooks": [
|
|
{
|
|
"type": "command",
|
|
"command": "if [[ $TOOL_ARGS == *\".env\"* ]]; then chmod 600 $TOOL_ARGS; fi; exit 0"
|
|
}
|
|
]
|
|
}
|
|
]
|
|
}
|
|
}
|
|
```
|
|
|
|
### 8. Notify on Long Sessions
|
|
|
|
Alert when session ends:
|
|
|
|
```json
|
|
{
|
|
"hooks": {
|
|
"SessionEnd": [
|
|
{
|
|
"matcher": "*",
|
|
"hooks": [
|
|
{
|
|
"type": "command",
|
|
"command": "osascript -e 'display notification \"Session ended\" with title \"Claude Code\"' && exit 0"
|
|
}
|
|
]
|
|
}
|
|
]
|
|
}
|
|
}
|
|
```
|
|
|
|
## Advanced Patterns
|
|
|
|
### Multiple Hooks per Event
|
|
|
|
Run several hooks in sequence:
|
|
|
|
```json
|
|
{
|
|
"hooks": {
|
|
"PostToolUse": [
|
|
{
|
|
"matcher": "Write|Edit",
|
|
"hooks": [
|
|
{
|
|
"type": "command",
|
|
"command": "npx prettier --write $TOOL_ARGS"
|
|
},
|
|
{
|
|
"type": "command",
|
|
"command": "npx eslint --fix $TOOL_ARGS"
|
|
},
|
|
{
|
|
"type": "command",
|
|
"command": "git add $TOOL_ARGS"
|
|
}
|
|
]
|
|
}
|
|
]
|
|
}
|
|
}
|
|
```
|
|
|
|
Hooks run in order; if one fails, subsequent hooks may not run.
|
|
|
|
### Conditional Execution
|
|
|
|
Use bash conditionals:
|
|
|
|
```json
|
|
{
|
|
"hooks": {
|
|
"PostToolUse": [
|
|
{
|
|
"matcher": "Write",
|
|
"hooks": [
|
|
{
|
|
"type": "command",
|
|
"command": "if [[ $TOOL_ARGS == *.js ]]; then npm run format $TOOL_ARGS; fi; exit 0"
|
|
}
|
|
]
|
|
}
|
|
]
|
|
}
|
|
}
|
|
```
|
|
|
|
### Script-Based Hooks
|
|
|
|
Call external scripts for complex logic:
|
|
|
|
```json
|
|
{
|
|
"hooks": {
|
|
"PreToolUse": [
|
|
{
|
|
"matcher": "Write",
|
|
"hooks": [
|
|
{
|
|
"type": "command",
|
|
"command": "$CLAUDE_PROJECT_DIR/.claude/hooks/validate-write.sh $TOOL_ARGS",
|
|
"timeout": 60
|
|
}
|
|
]
|
|
}
|
|
]
|
|
}
|
|
}
|
|
```
|
|
|
|
**validate-write.sh**:
|
|
```bash
|
|
#!/bin/bash
|
|
file=$1
|
|
|
|
# Check file size
|
|
if [ -f "$file" ] && [ $(wc -c < "$file") -gt 1000000 ]; then
|
|
echo "Error: File too large" >&2
|
|
exit 2
|
|
fi
|
|
|
|
# Check for secrets
|
|
if grep -qiE '(password|api[_-]?key)' "$file" 2>/dev/null; then
|
|
echo "Error: Possible secret detected" >&2
|
|
exit 2
|
|
fi
|
|
|
|
exit 0
|
|
```
|
|
|
|
### JSON Output for Advanced Control
|
|
|
|
Return structured data (advanced):
|
|
|
|
```json
|
|
{
|
|
"hooks": {
|
|
"PreToolUse": [
|
|
{
|
|
"matcher": "Bash",
|
|
"hooks": [
|
|
{
|
|
"type": "command",
|
|
"command": "echo '{\"continue\": true, \"decision\": \"allow\"}'"
|
|
}
|
|
]
|
|
}
|
|
]
|
|
}
|
|
}
|
|
```
|
|
|
|
## Settings File Hierarchy
|
|
|
|
### Load Order (Highest to Lowest Priority)
|
|
|
|
1. `.claude/settings.local.json` - Project-specific, user-only (gitignored)
|
|
2. `.claude/settings.json` - Project-specific, team-shared (in git)
|
|
3. `~/.claude/settings.json` - Global, user-only
|
|
|
|
### Use Cases
|
|
|
|
**Global (~/.claude/settings.json)**:
|
|
- Personal preferences
|
|
- Universal logging
|
|
- Desktop notifications
|
|
|
|
**Project (.claude/settings.json)**:
|
|
- Team standards
|
|
- Project-specific automation
|
|
- Shared workflows
|
|
|
|
**Local (.claude/settings.local.json)**:
|
|
- Personal overrides
|
|
- Development experiments
|
|
- User-specific credentials
|
|
|
|
## Testing Hooks
|
|
|
|
### 1. Start Simple
|
|
|
|
Test with basic echo:
|
|
|
|
```json
|
|
{
|
|
"hooks": {
|
|
"PostToolUse": [
|
|
{
|
|
"matcher": "Write",
|
|
"hooks": [
|
|
{
|
|
"type": "command",
|
|
"command": "echo 'Hook triggered for: $TOOL_ARGS' && exit 0"
|
|
}
|
|
]
|
|
}
|
|
]
|
|
}
|
|
}
|
|
```
|
|
|
|
### 2. Verify Environment Variables
|
|
|
|
```json
|
|
{
|
|
"hooks": {
|
|
"SessionStart": [
|
|
{
|
|
"matcher": "*",
|
|
"hooks": [
|
|
{
|
|
"type": "command",
|
|
"command": "echo \"Project: $CLAUDE_PROJECT_DIR\" && exit 0"
|
|
}
|
|
]
|
|
}
|
|
]
|
|
}
|
|
}
|
|
```
|
|
|
|
### 3. Test Exit Codes
|
|
|
|
```json
|
|
{
|
|
"hooks": {
|
|
"PreToolUse": [
|
|
{
|
|
"matcher": "Write",
|
|
"hooks": [
|
|
{
|
|
"type": "command",
|
|
"command": "echo 'This should block' >&2 && exit 2"
|
|
}
|
|
]
|
|
}
|
|
]
|
|
}
|
|
}
|
|
```
|
|
|
|
Try writing a file - should be blocked.
|
|
|
|
### 4. Check Timeout
|
|
|
|
```json
|
|
{
|
|
"hooks": {
|
|
"PostToolUse": [
|
|
{
|
|
"matcher": "Write",
|
|
"hooks": [
|
|
{
|
|
"type": "command",
|
|
"command": "sleep 5 && echo 'Done'",
|
|
"timeout": 3
|
|
}
|
|
]
|
|
}
|
|
]
|
|
}
|
|
}
|
|
```
|
|
|
|
Should timeout after 3 seconds.
|
|
|
|
## Common Use Cases
|
|
|
|
### Code Quality Enforcement
|
|
|
|
```json
|
|
{
|
|
"hooks": {
|
|
"PostToolUse": [
|
|
{
|
|
"matcher": "Write|Edit",
|
|
"hooks": [
|
|
{
|
|
"type": "command",
|
|
"command": "npx prettier --write $TOOL_ARGS && npx eslint --fix $TOOL_ARGS && exit 0"
|
|
}
|
|
]
|
|
}
|
|
]
|
|
}
|
|
}
|
|
```
|
|
|
|
### Security Scanning
|
|
|
|
```json
|
|
{
|
|
"hooks": {
|
|
"PreToolUse": [
|
|
{
|
|
"matcher": "Write",
|
|
"hooks": [
|
|
{
|
|
"type": "command",
|
|
"command": "if grep -qE '(sk-[a-zA-Z0-9]{48}|ghp_[a-zA-Z0-9]{36})' $TOOL_ARGS 2>/dev/null; then echo 'API key detected!' >&2; exit 2; fi; exit 0"
|
|
}
|
|
]
|
|
}
|
|
]
|
|
}
|
|
}
|
|
```
|
|
|
|
### Automatic Documentation
|
|
|
|
```json
|
|
{
|
|
"hooks": {
|
|
"PostToolUse": [
|
|
{
|
|
"matcher": "Write",
|
|
"hooks": [
|
|
{
|
|
"type": "command",
|
|
"command": "if [[ $TOOL_ARGS == *.js ]]; then npx jsdoc2md $TOOL_ARGS >> API.md; fi; exit 0"
|
|
}
|
|
]
|
|
}
|
|
]
|
|
}
|
|
}
|
|
```
|
|
|
|
### Change Tracking
|
|
|
|
```json
|
|
{
|
|
"hooks": {
|
|
"PostToolUse": [
|
|
{
|
|
"matcher": "*",
|
|
"hooks": [
|
|
{
|
|
"type": "command",
|
|
"command": "echo \"$(date): $TOOL_NAME $TOOL_ARGS\" >> .claude/audit.log && exit 0"
|
|
}
|
|
]
|
|
}
|
|
]
|
|
}
|
|
}
|
|
```
|
|
|
|
## Troubleshooting
|
|
|
|
### Hook Not Triggering
|
|
|
|
**Issue**: Hook doesn't run when expected
|
|
|
|
**Solutions**:
|
|
1. Check matcher pattern matches tool name
|
|
2. Verify settings file location and syntax (valid JSON)
|
|
3. Restart Claude Code to reload settings
|
|
4. Test with simple echo command first
|
|
|
|
### Hook Fails Silently
|
|
|
|
**Issue**: Hook runs but doesn't work as expected
|
|
|
|
**Solutions**:
|
|
1. Check exit codes (0 for success)
|
|
2. Verify command syntax in terminal first
|
|
3. Check timeout isn't too short
|
|
4. Look at stderr output
|
|
|
|
### Command Not Found
|
|
|
|
**Issue**: Hook says command not found
|
|
|
|
**Solutions**:
|
|
1. Use full path to command: `/usr/local/bin/prettier`
|
|
2. Check command is in PATH
|
|
3. Test command in terminal first
|
|
|
|
### Permission Denied
|
|
|
|
**Issue**: Hook can't execute script
|
|
|
|
**Solutions**:
|
|
```bash
|
|
chmod +x .claude/hooks/script.sh
|
|
```
|
|
|
|
## Best Practices
|
|
|
|
### 1. Always Exit with Code
|
|
|
|
```bash
|
|
# Good - explicit exit
|
|
command && exit 0
|
|
|
|
# Bad - implicit exit might vary
|
|
command
|
|
```
|
|
|
|
### 2. Use Timeouts for Long Operations
|
|
|
|
```json
|
|
{
|
|
"type": "command",
|
|
"command": "npm test",
|
|
"timeout": 120 // Don't let tests run forever
|
|
}
|
|
```
|
|
|
|
### 3. Handle Errors Gracefully
|
|
|
|
```bash
|
|
# Check if file exists before processing
|
|
if [ -f "$TOOL_ARGS" ]; then
|
|
process_file "$TOOL_ARGS"
|
|
else
|
|
echo "File not found" >&2
|
|
fi
|
|
exit 0 # Don't block on missing file
|
|
```
|
|
|
|
### 4. Use Scripts for Complex Logic
|
|
|
|
Don't put complex bash in JSON:
|
|
|
|
```json
|
|
// Bad
|
|
{
|
|
"command": "if [ -f $TOOL_ARGS ]; then cat $TOOL_ARGS | grep pattern | wc -l; fi && exit 0"
|
|
}
|
|
|
|
// Good
|
|
{
|
|
"command": "$CLAUDE_PROJECT_DIR/.claude/hooks/check-file.sh $TOOL_ARGS"
|
|
}
|
|
```
|
|
|
|
### 5. Test Before Deploying
|
|
|
|
Test in `.claude/settings.local.json` before adding to `.claude/settings.json`.
|
|
|
|
### 6. Document Your Hooks
|
|
|
|
Add comments in settings:
|
|
|
|
```json
|
|
{
|
|
"hooks": {
|
|
"PostToolUse": [
|
|
{
|
|
"matcher": "Write|Edit",
|
|
"hooks": [
|
|
{
|
|
"type": "command",
|
|
"command": "npx prettier --write $TOOL_ARGS",
|
|
"_comment": "Auto-format all JS/TS files on save"
|
|
}
|
|
]
|
|
}
|
|
]
|
|
}
|
|
}
|
|
```
|
|
|
|
(Note: JSON doesn't officially support comments, but Claude Code ignores `_comment` fields)
|
|
|
|
## Security Considerations
|
|
|
|
### Validate Input
|
|
|
|
```bash
|
|
# Sanitize TOOL_ARGS before use
|
|
if [[ ! $TOOL_ARGS =~ ^[a-zA-Z0-9/_.-]+$ ]]; then
|
|
echo "Invalid file path" >&2
|
|
exit 2
|
|
fi
|
|
```
|
|
|
|
### Limit Permissions
|
|
|
|
Don't use `*` matcher unless necessary:
|
|
|
|
```json
|
|
// Specific
|
|
"matcher": "Write|Edit"
|
|
|
|
// Too broad
|
|
"matcher": "*"
|
|
```
|
|
|
|
### Avoid Destructive Commands
|
|
|
|
```bash
|
|
# Dangerous
|
|
rm -rf $TOOL_ARGS
|
|
|
|
# Better - add safety checks
|
|
if [[ -f "$TOOL_ARGS" ]] && [[ "$TOOL_ARGS" != "/" ]]; then
|
|
rm "$TOOL_ARGS"
|
|
fi
|
|
```
|
|
|
|
### Use Local Settings for Sensitive Operations
|
|
|
|
Keep potentially dangerous hooks in `.claude/settings.local.json` (not shared).
|
|
|
|
## Resources
|
|
|
|
- [Official Hooks Documentation](https://docs.claude.com/en/docs/claude-code/hooks)
|
|
- [Slash Commands](../claude-code-slash-commands/SKILL.md) - For command-based automation
|
|
- [Plugin Development](../claude-code-plugins/SKILL.md) - For packaging hooks in plugins
|
|
|
|
---
|
|
|
|
**Remember**: Hooks are powerful automation tools. Start with simple, safe operations. Test thoroughly before deploying to teams. Use exit codes properly to control flow.
|