Files
claude-plugins/claude-code/skills/claude-code-hooks/SKILL.md

1050 lines
18 KiB
Markdown
Raw Normal View History

---
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.