Files
2025-11-29 18:47:48 +08:00

452 lines
13 KiB
Markdown

---
description: Add a hook configuration to automate plugin behavior at lifecycle events
argument-hint: [event-type] [matcher-pattern]
---
# Add Hook
Add or update hooks.json with a new hook configuration for automated behavior.
## Arguments
- `$1` (required): Event type (`PreToolUse`, `PostToolUse`, `SessionStart`, `SessionEnd`, `UserPromptSubmit`, `Notification`, `Stop`, `SubagentStop`, or `PreCompact`)
- `$2` (optional): Matcher pattern (e.g., `Write|Edit` or `.*`)
- `--plugin=<plugin-name>` (optional): Specify which plugin to add the hook to
**Usage:**
```
# From within a plugin directory
/plugin-development:add-hook PreToolUse "Write|Edit"
# From marketplace root, specifying plugin
/plugin-development:add-hook PreToolUse "Write|Edit" --plugin=plugin-development
```
## Template Variables
When generating hook configurations and scripts:
- `$1`: Event type
- `$2`: Matcher pattern (or default based on event type)
- `${CLAUDE_PLUGIN_ROOT}`: Plugin root path (use in all hook commands)
## Prerequisites
- Must be run from either:
- A plugin root directory (containing `.claude-plugin/plugin.json`), OR
- A marketplace root directory (containing `.claude-plugin/marketplace.json`)
- `hooks/` directory will be created if needed
## Instructions
### Validation
**IMPORTANT**: When running test commands for validation (checking directories, files, etc.), use `require_user_approval: false` since these are read-only checks.
1. **Detect context and target plugin** (output thoughts during this process):
a. Check if we're in a plugin directory:
- Look for `.claude-plugin/plugin.json` in current directory
- **Output**: "Checking for plugin directory..."
- If found:
- **Output**: "Found plugin.json - using current directory as target plugin"
- Use current directory as target plugin
- If not found:
- **Output**: "Not in a plugin directory, checking for marketplace..."
b. If not in plugin directory, check if we're in marketplace root:
- Look for `.claude-plugin/marketplace.json` in current directory
- If found:
- **Output**: "Found marketplace.json - this is a marketplace root"
- This is a marketplace root
- If not found:
- **Output**: "Error: Neither plugin.json nor marketplace.json found"
- Show error and exit
c. If in marketplace root, determine target plugin:
- Check if `--plugin=<name>` argument was provided
- If yes:
- **Output**: "Using plugin specified via --plugin argument: <name>"
- Use specified plugin name
- If no:
- **Output**: "No --plugin argument provided, discovering available plugins..."
- Discover available plugins and prompt user
d. **Discover available plugins** (when in marketplace root without --plugin):
- **Output**: "Reading marketplace.json..."
- Read `.claude-plugin/marketplace.json`
- Extract plugin names and sources from `plugins` array
- **Output**: "Found [N] plugin(s) in marketplace"
- Alternative: List directories in `plugins/` directory
- Present list to user: "Which plugin should I add the hook to?"
- Options format: `1. plugin-name-1 (description)`, `2. plugin-name-2 (description)`, etc.
- Wait for user selection
- **Output**: "Selected plugin: <plugin-name>"
e. **Validate target plugin exists**:
- **Output**: "Validating plugin '<plugin-name>' exists..."
- If plugin specified/selected, verify `plugins/<plugin-name>/.claude-plugin/plugin.json` exists
- If found:
- **Output**: "Plugin '<plugin-name>' validated successfully"
- If not found:
- **Output**: "Error: Plugin '<plugin-name>' not found"
- Show error: "Plugin '<plugin-name>' not found. Available plugins: [list]"
f. If neither plugin.json nor marketplace.json found:
- Show error: "Not in a plugin or marketplace directory. Please run from a plugin root or marketplace root."
2. **Validate event type**:
- Must be one of: `PreToolUse`, `PostToolUse`, `SessionStart`, `SessionEnd`, `UserPromptSubmit`, `Notification`, `Stop`, `SubagentStop`, `PreCompact`
3. **If no matcher provided, use sensible default based on event type**
4. **Set working directory**:
- If in plugin directory: Use current directory
- If in marketplace root: Use `plugins/<plugin-name>/` as working directory
### Event Types & Default Matchers
- **PreToolUse**: Default matcher `Write|Edit` (validation before writes)
- **PostToolUse**: Default matcher `Write|Edit` (formatting after writes)
- **SessionStart**: Default matcher `startup` (also supports: `resume`, `clear`, `compact`)
- **SessionEnd**: No matcher (triggers on session end with reason: `clear`, `logout`, `prompt_input_exit`, `other`)
- **UserPromptSubmit**: Default matcher `.*` (all prompts)
- **Notification**: No matcher (triggers on all notifications)
- **Stop**: No matcher (triggers when main agent stops)
- **SubagentStop**: No matcher (triggers when subagent stops)
- **PreCompact**: Default matcher `manual` (also supports: `auto`)
### Create or Update hooks.json
**Note**: All paths below are relative to the target plugin directory (determined in validation step).
1. Ensure `<plugin-dir>/hooks/` directory exists (use `require_user_approval: false`)
2. If `<plugin-dir>/hooks/hooks.json` doesn't exist, create it with this structure:
```json
{
"description": "Plugin automation hooks",
"hooks": {}
}
```
3. Add the new hook configuration based on event type:
#### PreToolUse Example
```json
{
"description": "Plugin automation hooks",
"hooks": {
"PreToolUse": [
{
"matcher": "$2 or default",
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PLUGIN_ROOT}/scripts/validate.sh",
"timeout": 30
}
]
}
]
}
}
```
#### PostToolUse Example
```json
{
"hooks": {
"PostToolUse": [
{
"matcher": "$2 or default",
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PLUGIN_ROOT}/scripts/format.sh",
"timeout": 30
}
]
}
]
}
}
```
#### SessionStart Example
```json
{
"hooks": {
"SessionStart": [
{
"matcher": "$2 or 'startup'",
"hooks": [
{
"type": "command",
"command": "echo '✓ Plugin loaded'"
}
]
}
]
}
}
```
### Create Hook Script (if needed)
If the event is PreToolUse or PostToolUse, create a corresponding script:
1. Ensure `<plugin-dir>/scripts/` directory exists (use `require_user_approval: false`)
2. For PreToolUse, create `<plugin-dir>/scripts/validate.sh`:
```bash
#!/usr/bin/env bash
set -euo pipefail
# Validation logic here
# Exit 0: allow
# Exit 2: block (with stderr message to Claude)
# Exit other: warning
echo "Validation passed"
exit 0
```
3. For PostToolUse, create `<plugin-dir>/scripts/format.sh`:
```bash
#!/usr/bin/env bash
set -euo pipefail
# Formatting logic here
# Always exits 0 for non-blocking
echo "Formatting complete"
exit 0
```
4. Make scripts executable:
```bash
chmod +x scripts/*.sh
```
### Update plugin.json
**IMPORTANT**: Only needed if using custom (non-standard) paths.
- **Standard setup** (hooks at `hooks/hooks.json`): No changes to `plugin.json` needed
- **Custom path**: Add `"hooks": "./custom/path/hooks.json"`
For standard setup:
```json
{
"name": "my-plugin"
}
```
### Provide Feedback
After adding the hook:
```
✓ Added $1 hook to <plugin-name>/hooks/hooks.json
✓ Matcher: $2 (or default)
✓ Created script: <plugin-name>/scripts/<script-name>.sh (if applicable)
Plugin: <plugin-name>
Hook configuration:
- Event: $1
- Matcher: $2
- Script: ${CLAUDE_PLUGIN_ROOT}/scripts/<script-name>.sh
Next steps:
1. Edit <plugin-name>/hooks/hooks.json to customize timeout or command
2. Edit <plugin-name>/scripts/<script-name>.sh with your logic
3. Test the hook:
- Install plugin with /plugin-development:test-local
- Trigger the event (e.g., use Write tool for PreToolUse)
4. Debug with: claude --debug
Exit codes for PreToolUse:
- 0: Allow operation
- 2: Block operation (stderr shown to Claude)
- Other: Warning (non-blocking)
Exit codes for UserPromptSubmit:
- 0: Allow prompt (stdout added as context)
- 2: Block prompt (stderr shown to user, prompt erased)
- Other: Warning (non-blocking)
Exit codes for Stop/SubagentStop:
- 0: Allow stop
- 2: Block stop, continue execution (stderr shown to Claude)
- Other: Warning (allows stop)
Exit codes for PostToolUse, SessionStart, SessionEnd, Notification, PreCompact:
- All non-blocking (informational)
```
## Example Usage
**Input**: `/plugin-development:add-hook PreToolUse "Write|Edit"`
**Result**:
- Creates or updates `hooks/hooks.json`
- Adds PreToolUse hook with Write|Edit matcher
- Creates `scripts/validate.sh`
- Makes script executable
- Provides usage instructions
**For complete details on hooks**, see:
- [Hooks reference documentation](/en/docs/claude-code/hooks)
- [Plugin hooks reference](/en/docs/claude-code/plugins-reference#hooks)
## Hook Event Details
| Event | Purpose | Can Block | Common Use Cases | Default Matcher |
|-------|---------|-----------|------------------|-----------------|
| **PreToolUse** | Validate before execution | Yes (exit 2) | Validate structure, check permissions | `Write\|Edit` |
| **PostToolUse** | React after execution | Partial* | Format files, run linters, update metadata | `Write\|Edit` |
| **SessionStart** | Setup at session start | No | Welcome message, check environment, init | `startup` |
| **SessionEnd** | Cleanup at session end | No | Save state, log statistics, cleanup | N/A (no matcher) |
| **UserPromptSubmit** | Validate/enhance prompts | Yes (exit 2) | Inject context, validate prompts, block sensitive | `.*` |
| **Notification** | React to notifications | No | Send alerts, log notifications | N/A (no matcher) |
| **Stop** | Control agent stoppage | Yes (exit 2) | Continue with tasks, validate completion | N/A (no matcher) |
| **SubagentStop** | Control subagent stoppage | Yes (exit 2) | Continue subagent, validate subagent results | N/A (no matcher) |
| **PreCompact** | Before context compact | No | Save state, log compact trigger | `manual` or `auto` |
\* PostToolUse can't prevent the tool (already ran) but can provide feedback to Claude with `"decision": "block"`
**Example hook structure**:
```json
{
"matcher": "Write|Edit",
"hooks": [{
"type": "command",
"command": "${CLAUDE_PLUGIN_ROOT}/scripts/validate.sh",
"timeout": 30
}]
}
```
## Matcher Patterns
Matchers use **regex patterns**:
- `Write`: Only Write tool
- `Write|Edit`: Write or Edit
- `Bash.*`: Bash with any arguments
- `.*`: All tools
- `Read|Grep|Glob`: Read operations
## Script Template: Validation (PreToolUse)
```bash
#!/usr/bin/env bash
set -euo pipefail
ERRS=()
# Validation checks
[ -f "required-file.txt" ] || ERRS+=("Missing required-file.txt")
[ -d "required-dir" ] || ERRS+=("Missing required-dir/")
# If errors, block with exit 2
if [ "${#ERRS[@]}" -gt 0 ]; then
printf "❌ Validation failed:\n" 1>&2
printf " %s\n" "${ERRS[@]}" 1>&2
exit 2 # Block the operation
fi
# Success
echo "✓ Validation passed"
exit 0
```
## Script Template: Formatting (PostToolUse)
```bash
#!/usr/bin/env bash
set -euo pipefail
# Format files (non-blocking)
# Example: run prettier, black, rustfmt, etc.
if command -v prettier &> /dev/null; then
prettier --write "**/*.{js,json,md}" 2>/dev/null || true
fi
echo "✓ Formatting complete"
exit 0
```
## Environment Variables
Available in hook scripts:
- `${CLAUDE_PLUGIN_ROOT}`: Absolute path to plugin root
- `$CLAUDE_PROJECT_DIR`: Project root directory (absolute path)
- `$CLAUDE_ENV_FILE`: File path for persisting environment variables (SessionStart hooks only)
- `$CLAUDE_CODE_REMOTE`: Set to "true" in web environment, unset in CLI
- All standard environment variables
Always use `${CLAUDE_PLUGIN_ROOT}` for portable paths in plugins.
## Best Practices & Common Mistakes
### ✅ Do This
- **Use ${CLAUDE_PLUGIN_ROOT}**: Portable script paths
- **Set timeouts**: 10-30 seconds typical, 300+ for slow ops like `npm install`
- **Fast scripts**: Keep runtime < 1 second when possible
- **Exit code 2 to block**: Only in PreToolUse for validation failures
- **Clear error messages**: Helpful stderr output
- **Make executable**: `chmod +x scripts/*.sh`
### ❌ Avoid This
- **Absolute paths**: `/Users/you/plugin/scripts/` (use `${CLAUDE_PLUGIN_ROOT}` instead)
- **Missing timeouts**: Slow operations without timeout values
- **Non-executable scripts**: Forgot to `chmod +x`
## Debugging Hooks
### Use debug mode
```bash
claude --debug
```
This shows:
- Hook registration
- Hook execution
- Exit codes
- Stdout/stderr output
### Test scripts directly
```bash
./scripts/validate.sh
echo $? # Check exit code
```
### Check hook configuration
```bash
cat hooks/hooks.json | jq .
```
## Validation Checklist
After adding a hook:
```
□ hooks/hooks.json created/updated
□ Hook event is valid (PreToolUse, etc.)
□ Matcher pattern is appropriate
□ Script created (if needed)
□ Script is executable (chmod +x)
□ Script uses ${CLAUDE_PLUGIN_ROOT}
□ Timeout set for long operations
□ plugin.json updated (only if using custom paths)
□ Tested with /plugin-development:test-local
```