Add or update hooks.json with a new hook configuration for automated behavior.
Add or update hooks.json with a new hook configuration for automated behavior.
/plugin marketplace add LucasXu0/claude-code-plugin/plugin install plugin-development@LucasXu0-flutter-toolsAdd or update hooks.json with a new hook configuration for automated behavior.
$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 toUsage:
# 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
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).claude-plugin/plugin.json), OR.claude-plugin/marketplace.json)hooks/ directory will be created if neededIMPORTANT: 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:
.claude-plugin/plugin.json in current directoryb. If not in plugin directory, check if we're in marketplace root:
.claude-plugin/marketplace.json in current directoryc. If in marketplace root, determine target plugin:
--plugin=<name> argument was providedd. Discover available plugins (when in marketplace root without --plugin):
.claude-plugin/marketplace.jsonplugins arrayplugins/ directory1. plugin-name-1 (description), 2. plugin-name-2 (description), etc.e. Validate target plugin exists:
plugins/<plugin-name>/.claude-plugin/plugin.json existsf. If neither plugin.json nor marketplace.json found:
Validate event type:
PreToolUse, PostToolUse, SessionStart, SessionEnd, UserPromptSubmit, Notification, Stop, SubagentStop, PreCompactIf no matcher provided, use sensible default based on event type
Set working directory:
plugins/<plugin-name>/ as working directoryWrite|Edit (validation before writes)Write|Edit (formatting after writes)startup (also supports: resume, clear, compact)clear, logout, prompt_input_exit, other).* (all prompts)manual (also supports: auto)Note: All paths below are relative to the target plugin directory (determined in validation step).
<plugin-dir>/hooks/ directory exists (use require_user_approval: false)<plugin-dir>/hooks/hooks.json doesn't exist, create it with this structure:{
"description": "Plugin automation hooks",
"hooks": {}
}
{
"description": "Plugin automation hooks",
"hooks": {
"PreToolUse": [
{
"matcher": "$2 or default",
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PLUGIN_ROOT}/scripts/validate.sh",
"timeout": 30
}
]
}
]
}
}
{
"hooks": {
"PostToolUse": [
{
"matcher": "$2 or default",
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PLUGIN_ROOT}/scripts/format.sh",
"timeout": 30
}
]
}
]
}
}
{
"hooks": {
"SessionStart": [
{
"matcher": "$2 or 'startup'",
"hooks": [
{
"type": "command",
"command": "echo '✓ Plugin loaded'"
}
]
}
]
}
}
If the event is PreToolUse or PostToolUse, create a corresponding script:
<plugin-dir>/scripts/ directory exists (use require_user_approval: false)<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
<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
chmod +x scripts/*.sh
IMPORTANT: Only needed if using custom (non-standard) paths.
hooks/hooks.json): No changes to plugin.json needed"hooks": "./custom/path/hooks.json"For standard setup:
{
"name": "my-plugin"
}
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)
Input: /plugin-development:add-hook PreToolUse "Write|Edit"
Result:
hooks/hooks.jsonscripts/validate.shFor complete details on hooks, see:
| 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
}]
}
Matchers use regex patterns:
Write: Only Write toolWrite|Edit: Write or EditBash.*: Bash with any arguments.*: All toolsRead|Grep|Glob: Read operations#!/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
#!/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
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 CLIAlways use ${CLAUDE_PLUGIN_ROOT} for portable paths in plugins.
npm installchmod +x scripts/*.sh/Users/you/plugin/scripts/ (use ${CLAUDE_PLUGIN_ROOT} instead)chmod +xclaude --debug
This shows:
./scripts/validate.sh
echo $? # Check exit code
cat hooks/hooks.json | jq .
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