Guide for creating event-driven hooks that validate, log, or modify Claude's behavior. Use when the user asks to "create a hook", "add PreToolUse hook", "validate tool use", "hook events", "hooks.json", or mentions hook events (PreToolUse, PostToolUse, Stop).
From claude-plugin-devnpx claudepluginhub nthplusio/functional-claude --plugin claude-plugin-devThis skill uses the workspace's default tool permissions.
references/patterns.mdExecutes pre-written implementation plans: critically reviews, follows bite-sized steps exactly, runs verifications, tracks progress with checkpoints, uses git worktrees, stops on blockers.
Guides idea refinement into designs: explores context, asks questions one-by-one, proposes approaches, presents sections for approval, writes/review specs before coding.
Dispatches parallel agents to independently tackle 2+ tasks like separate test failures or subsystems without shared state or dependencies.
Guide for creating event-driven hooks in hooks/hooks.json.
{
"description": "What these hooks do",
"hooks": {
"PreToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PLUGIN_ROOT}/hooks/validate.js",
"timeout": 30
}
]
}
]
}
}
Command hooks execute a script (see format above). Prompt-based hooks let Claude evaluate conditions — useful for semantic checks hard to script:
{
"type": "prompt",
"prompt": "Check if this file modification follows coding standards. If it violates any standards, explain why and set permissionDecision to 'deny'."
}
| Event | When It Fires |
|---|---|
SessionStart | Session begins or resumes |
UserPromptSubmit | User submits a prompt |
PreToolUse | Before tool execution |
PostToolUse | After tool succeeds |
Stop | Claude finishes responding |
SubagentStart | When spawning a subagent |
SubagentStop | When subagent finishes |
PreCompact | Before context compaction |
SessionEnd | Session terminates |
Notification | Claude sends notifications |
Setup | With --init or --maintenance flags |
| Variable | Scope | Description |
|---|---|---|
${CLAUDE_PLUGIN_ROOT} | Plugin hooks | Absolute path to plugin |
"$CLAUDE_PROJECT_DIR" | Project hooks | Project root (quoted) |
Critical: Plugin hooks use ${CLAUDE_PLUGIN_ROOT} with braces. Project hooks use "$CLAUDE_PROJECT_DIR" with quotes.
| Code | Meaning |
|---|---|
| 0 | Success — allow action |
| 2 | Block — deny action, show stderr |
| Other | Non-blocking error |
Write|Edit — File modificationsBash — Shell commands* or "" — All toolsmcp__.* — All MCP toolsHooks can be enabled/disabled via .local.md config:
const config = parseLocalMd(pluginRoot);
if (!config.hooks?.validation_enabled) {
process.exit(0); // Skip hook
}
See /claude-plugin-dev:plugin-settings for configuration patterns.
{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "allow",
"permissionDecisionReason": "Explanation"
}
}
Permission values: allow, deny, ask
#!/bin/bash
# Hook: PreToolUse - Description
# Exit 0 = allow, Exit 2 = block (stderr shown to Claude)
set -e
INPUT=$(cat)
TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // "unknown"')
TOOL_INPUT=$(echo "$INPUT" | jq -c '.tool_input // {}')
# ... validation logic ...
exit 0
Never use #!/usr/bin/env node — it fails on macOS when Node is installed via version managers (nvm, fnm, volta). Use this polyglot preamble instead:
#!/bin/sh
":" //; export PATH="/opt/homebrew/bin:/usr/local/bin:$HOME/.volta/bin:$HOME/.fnm/aliases/default/bin:$HOME/.asdf/shims:$HOME/.local/share/mise/shims:$HOME/.local/bin:$PATH"
":" //; command -v node >/dev/null 2>&1 || { [ -s "${NVM_DIR:-$HOME/.nvm}/nvm.sh" ] && . "${NVM_DIR:-$HOME/.nvm}/nvm.sh" 2>/dev/null; }
":" //; exec node "$0" "$@"
'use strict';
let input = '';
process.stdin.setEncoding('utf8');
process.stdin.on('data', chunk => input += chunk);
process.stdin.on('end', () => {
try {
const data = JSON.parse(input || '{}');
// ... logic ...
console.log(JSON.stringify({ continue: true }));
} catch (err) {
process.exit(0); // Always fail open
}
});
For detailed patterns, bash examples, and best practices: read references/patterns.md.