Guide for creating event-driven hooks for Claude Code. Use when automating responses to tool calls, lifecycle events, or implementing custom validations.
Scanned 2/12/2026
Install via CLI
openskills install majiayu000/claude-skill-registry---
name: claude-hooks
description: Guide for creating event-driven hooks for Claude Code. Use when automating responses to tool calls, lifecycle events, or implementing custom validations.
license: MIT
allowed-tools:
- Read
- Write
- Edit
- Bash
- Glob
---
# Claude Code Hooks
Guide for creating hooks that execute shell commands or scripts in response to Claude Code events and tool calls.
## When to Use This Skill
Activate this skill when:
- Creating event-driven automations
- Implementing custom validation or formatting
- Integrating with external tools and services
- Setting up project-specific workflows
- Responding to tool execution events
## What Are Hooks?
Hooks are shell commands that execute automatically in response to specific events:
- **Tool Call Hooks**: Trigger before/after specific tool calls
- **Lifecycle Hooks**: Trigger on plugin install/uninstall
- **User Prompt Hooks**: Trigger when users submit prompts
- **Custom Events**: Application-specific trigger points
## Hook Configuration
### Location
Hooks are configured in:
- Plugin: `<plugin-root>/.claude-plugin/hooks.json`
- User-level: `.claude/hooks.json`
- Plugin manifest: Inline in `plugin.json`
### File Structure
**Standalone hooks.json:**
```json
{
"onToolCall": {
"Write": {
"before": ["./hooks/format-check.sh"],
"after": ["./hooks/lint.sh"]
},
"Bash": {
"before": ["./hooks/validate-command.sh"]
}
},
"onInstall": ["./hooks/setup.sh"],
"onUninstall": ["./hooks/cleanup.sh"],
"onUserPromptSubmit": ["./hooks/log-prompt.sh"]
}
```
**Inline in plugin.json:**
```json
{
"hooks": {
"onToolCall": {
"Write": {
"after": ["prettier --write {{file_path}}"]
}
}
}
}
```
## Hook Types
### Tool Call Hooks
Execute before or after specific tool calls.
**Available Tools:**
- `Read`, `Write`, `Edit`, `MultiEdit`
- `Bash`, `BashOutput`
- `Glob`, `Grep`
- `Task`, `Skill`, `SlashCommand`
- `TodoWrite`
- `WebFetch`, `WebSearch`
- `AskUserQuestion`
**Example:**
```json
{
"onToolCall": {
"Write": {
"before": [
"echo 'Writing file: {{file_path}}'",
"./hooks/backup.sh {{file_path}}"
],
"after": [
"prettier --write {{file_path}}",
"git add {{file_path}}"
]
},
"Edit": {
"after": ["eslint --fix {{file_path}}"]
}
}
}
```
### Lifecycle Hooks
Execute during plugin installation/uninstallation.
```json
{
"onInstall": [
"./hooks/setup-dependencies.sh",
"npm install",
"echo 'Plugin installed successfully'"
],
"onUninstall": [
"./hooks/cleanup.sh",
"echo 'Plugin uninstalled'"
]
}
```
### User Prompt Submit Hook
Execute when user submits a prompt:
```json
{
"onUserPromptSubmit": [
"./hooks/log-interaction.sh '{{prompt}}'",
"./hooks/check-context.sh"
]
}
```
## Hook Variables
Hooks have access to context-specific variables using `{{variable}}` syntax.
### Tool Call Variables
Different tools provide different variables:
**Write Tool:**
- `{{file_path}}`: Path to file being written
- `{{content}}`: Content being written (before hooks only)
**Edit Tool:**
- `{{file_path}}`: Path to file being edited
- `{{old_string}}`: String being replaced
- `{{new_string}}`: Replacement string
**Bash Tool:**
- `{{command}}`: Command being executed
**Read Tool:**
- `{{file_path}}`: Path to file being read
### Global Variables
Available in all hooks:
- `{{cwd}}`: Current working directory
- `{{timestamp}}`: Current Unix timestamp
- `{{user}}`: Current user
- `{{plugin_root}}`: Plugin installation directory
### User Prompt Variables
- `{{prompt}}`: User's submitted prompt text
## Hook Examples
### Auto-Format on Write
```json
{
"onToolCall": {
"Write": {
"after": [
"prettier --write {{file_path}}",
"eslint --fix {{file_path}}"
]
}
}
}
```
### Pre-Commit Validation
```json
{
"onToolCall": {
"Bash": {
"before": ["./hooks/validate-git-command.sh '{{command}}'"]
}
}
}
```
**validate-git-command.sh:**
```bash
#!/bin/bash
COMMAND="$1"
# Block force push to main/master
if [[ "$COMMAND" =~ "git push --force" ]] && [[ "$COMMAND" =~ "main|master" ]]; then
echo "ERROR: Force push to main/master is not allowed"
exit 1
fi
exit 0
```
### Automatic Backups
```json
{
"onToolCall": {
"Write": {
"before": ["cp {{file_path}} {{file_path}}.backup"]
},
"Edit": {
"before": ["cp {{file_path}} {{file_path}}.backup"]
}
}
}
```
### Logging and Analytics
```json
{
"onToolCall": {
"Write": {
"after": ["./hooks/log-file-change.sh {{file_path}}"]
}
},
"onUserPromptSubmit": ["./hooks/log-prompt.sh '{{prompt}}'"]
}
```
**log-file-change.sh:**
```bash
#!/bin/bash
FILE="$1"
TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
echo "$TIMESTAMP - Modified: $FILE" >> .claude/file-changes.log
```
### Integration with External Tools
```json
{
"onToolCall": {
"Write": {
"after": [
"notify-send 'File Updated' 'Modified {{file_path}}'",
"curl -X POST https://api.example.com/notify -d 'file={{file_path}}'"
]
}
}
}
```
## Hook Execution
### Execution Order
Multiple hooks execute in array order:
```json
{
"onToolCall": {
"Write": {
"after": [
"echo 'Step 1'", // Runs first
"echo 'Step 2'", // Runs second
"echo 'Step 3'" // Runs third
]
}
}
}
```
### Exit Codes
**Before Hooks:**
- Exit code `0`: Continue with tool execution
- Exit code non-zero: **Block tool execution**, show error to user
**After Hooks:**
- Exit codes are logged but don't affect tool execution
- Tool has already completed
### Error Handling
```bash
#!/bin/bash
# Before hook - blocks tool on error
if [[ ! -f "$1" ]]; then
echo "ERROR: File does not exist"
exit 1 # Blocks tool execution
fi
# Validation passed
exit 0
```
## Best Practices
### Keep Hooks Fast
Hooks block execution - keep them lightweight:
```json
{
"onToolCall": {
"Write": {
// ✅ Fast linter
"after": ["eslint --fix {{file_path}}"]
// ❌ Slow test suite
// "after": ["npm test"]
}
}
}
```
### Use Absolute Paths
Reference scripts with paths relative to plugin:
```json
{
"onInstall": ["${CLAUDE_PLUGIN_ROOT}/hooks/setup.sh"]
}
```
### Validate Input
Always validate hook variables:
```bash
#!/bin/bash
FILE="$1"
if [[ -z "$FILE" ]]; then
echo "ERROR: No file path provided"
exit 1
fi
if [[ ! -f "$FILE" ]]; then
echo "ERROR: File does not exist: $FILE"
exit 1
fi
```
### Provide Clear Feedback
```bash
#!/bin/bash
echo "Running pre-commit checks..."
if ! npm run lint; then
echo "❌ Linting failed. Please fix errors before committing."
exit 1
fi
echo "✅ All checks passed"
exit 0
```
### Handle Edge Cases
```bash
#!/bin/bash
# Handle files with spaces in names
FILE="$1"
# Validate file type
if [[ ! "$FILE" =~ \.(js|ts|jsx|tsx)$ ]]; then
# Skip non-JavaScript files silently
exit 0
fi
# Run formatter
prettier --write "$FILE"
```
## Security Considerations
### Validate Commands
Before hooks can block dangerous operations:
```json
{
"onToolCall": {
"Bash": {
"before": ["./hooks/validate-command.sh '{{command}}'"]
}
}
}
```
**validate-command.sh:**
```bash
#!/bin/bash
COMMAND="$1"
# Block dangerous patterns
DANGEROUS_PATTERNS=(
"rm -rf /"
"dd if="
"mkfs"
"> /dev/sda"
)
for pattern in "${DANGEROUS_PATTERNS[@]}"; do
if [[ "$COMMAND" =~ $pattern ]]; then
echo "ERROR: Dangerous command blocked: $pattern"
exit 1
fi
done
exit 0
```
### Limit Hook Scope
Only hook necessary tools:
```json
{
// ✅ Specific tools only
"onToolCall": {
"Write": { "after": ["./format.sh {{file_path}}"] }
}
// ❌ Don't hook everything unnecessarily
}
```
### Sanitize Variables
```bash
#!/bin/bash
# Sanitize file path
FILE=$(realpath "$1")
# Ensure file is within project
if [[ ! "$FILE" =~ ^$(pwd) ]]; then
echo "ERROR: File outside project directory"
exit 1
fi
```
## Debugging Hooks
### Enable Verbose Output
```json
{
"onToolCall": {
"Write": {
"before": ["set -x; ./hooks/debug.sh {{file_path}}; set +x"]
}
}
}
```
### Log Hook Execution
```bash
#!/bin/bash
LOG_FILE=".claude/hooks.log"
TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
echo "$TIMESTAMP - Hook: $0, Args: $@" >> "$LOG_FILE"
# Rest of hook logic...
```
### Test Hooks Manually
```bash
# Test hook with sample data
./hooks/format.sh "src/main.js"
# Check exit code
echo $?
```
## Common Hook Patterns
### Auto-Format Pipeline
```json
{
"onToolCall": {
"Write": {
"after": [
"prettier --write {{file_path}}",
"eslint --fix {{file_path}}"
]
},
"Edit": {
"after": [
"prettier --write {{file_path}}",
"eslint --fix {{file_path}}"
]
}
}
}
```
### Test on Write
```json
{
"onToolCall": {
"Write": {
"after": ["./hooks/run-relevant-tests.sh {{file_path}}"]
}
}
}
```
### Git Integration
```json
{
"onToolCall": {
"Write": {
"after": ["git add {{file_path}}"]
},
"Edit": {
"after": ["git add {{file_path}}"]
}
}
}
```
## Troubleshooting
### Hook Not Executing
- Check hook file has execute permissions: `chmod +x hooks/script.sh`
- Verify path is correct relative to plugin root
- Check JSON syntax in hooks.json
- Look for errors in Claude Code logs
### Hook Blocking Tool
- Check exit code of before hooks
- Add debug logging
- Test hook script manually
- Verify validation logic
### Variables Not Substituting
- Check variable name spelling: `{{file_path}}` not `{{filepath}}`
- Verify variable is available for that tool
- Quote variables in bash: `"{{file_path}}"`
## Templates
Reference templates for common hook configurations:
```
claude-hooks/
└── templates/
├── plugin-hook.md # Plugin hook configuration example
└── skill-hook.md # Skill/subagent frontmatter hooks example
```
### Plugin Hook Template
Example configuration for defining hooks in a plugin's `hooks/hooks.json`:
- PostToolUse hook with `Write|Edit` matcher
- Uses `${CLAUDE_PLUGIN_ROOT}` for script references
- Includes timeout configuration
### Skill Hook Template
Example frontmatter for embedding hooks directly in skills:
- Supported events: PreToolUse, PostToolUse, Stop
- Hooks scoped to component lifecycle
- Runs only when skill/subagent is active
## References
For more information:
- Claude Code Hooks Documentation: https://code.claude.com/docs/en/hooks
- Plugin Configuration: https://code.claude.com/docs/en/plugins#hooks
No comments yet. Be the first to comment!