13 KiB
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, orPreCompact)$2(optional): Matcher pattern (e.g.,Write|Editor.*)--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)
- A plugin root directory (containing
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.
-
Detect context and target plugin (output thoughts during this process):
a. Check if we're in a plugin directory:
- Look for
.claude-plugin/plugin.jsonin 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.jsonin 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: "
- 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
pluginsarray - 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: "
e. Validate target plugin exists:
- Output: "Validating plugin '' exists..."
- If plugin specified/selected, verify
plugins/<plugin-name>/.claude-plugin/plugin.jsonexists - If found:
- Output: "Plugin '' validated successfully"
- If not found:
- Output: "Error: Plugin '' not found"
- Show error: "Plugin '' 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."
- Look for
-
Validate event type:
- Must be one of:
PreToolUse,PostToolUse,SessionStart,SessionEnd,UserPromptSubmit,Notification,Stop,SubagentStop,PreCompact
- Must be one of:
-
If no matcher provided, use sensible default based on event type
-
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).
- Ensure
<plugin-dir>/hooks/directory exists (userequire_user_approval: false) - If
<plugin-dir>/hooks/hooks.jsondoesn't exist, create it with this structure:
{
"description": "Plugin automation hooks",
"hooks": {}
}
- Add the new hook configuration based on event type:
PreToolUse Example
{
"description": "Plugin automation hooks",
"hooks": {
"PreToolUse": [
{
"matcher": "$2 or default",
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PLUGIN_ROOT}/scripts/validate.sh",
"timeout": 30
}
]
}
]
}
}
PostToolUse Example
{
"hooks": {
"PostToolUse": [
{
"matcher": "$2 or default",
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PLUGIN_ROOT}/scripts/format.sh",
"timeout": 30
}
]
}
]
}
}
SessionStart Example
{
"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:
- Ensure
<plugin-dir>/scripts/directory exists (userequire_user_approval: false) - For PreToolUse, create
<plugin-dir>/scripts/validate.sh:
#!/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
- For PostToolUse, create
<plugin-dir>/scripts/format.sh:
#!/usr/bin/env bash
set -euo pipefail
# Formatting logic here
# Always exits 0 for non-blocking
echo "Formatting complete"
exit 0
- Make scripts executable:
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 toplugin.jsonneeded - Custom path: Add
"hooks": "./custom/path/hooks.json"
For standard setup:
{
"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:
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:
{
"matcher": "Write|Edit",
"hooks": [{
"type": "command",
"command": "${CLAUDE_PLUGIN_ROOT}/scripts/validate.sh",
"timeout": 30
}]
}
Matcher Patterns
Matchers use regex patterns:
Write: Only Write toolWrite|Edit: Write or EditBash.*: Bash with any arguments.*: All toolsRead|Grep|Glob: Read operations
Script Template: Validation (PreToolUse)
#!/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)
#!/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
claude --debug
This shows:
- Hook registration
- Hook execution
- Exit codes
- Stdout/stderr output
Test scripts directly
./scripts/validate.sh
echo $? # Check exit code
Check hook configuration
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