| name | hooks-builder |
| description | Use when the user asks to design, build, optimize, or audit Claude Code HOOKS โ event-driven shell commands in settings.json that fire on SessionStart, PreToolUse, PostToolUse, Stop, etc. Triggers โ "build a hook", "add a hook", "run X every time I edit/commit/start a session", "block tool X whenโฆ", "audit my hooks", or `/hooks-builder`. Sibling of `/skill-builder`, `/agent-builder`, `/routines-builder`, `/agents-team-builder`, `/plugin-builder` โ this one is the event-driven-local specialist (the hooks lane `/routines-builder` hands off to). |
| argument-hint | ["what should fire","on which event"] |
| disable-model-invocation | true |
/hooks-builder
Builds Claude Code hooks: event-driven commands the harness executes deterministically on matched events. A hook is the right tool when the behavior must happen EVERY time, mechanically โ memory and prompts cannot fulfill "always do X on Y"; only the hook layer can. Schema, events, and the stdin/exit-code protocol live in reference.md.
What this skill does
- Build a new hook: decision gate โ discovery โ script + config โ supervised first fire โ register.
- Optimize an existing hook (read it first).
- Audit all configured hooks (user + project + plugin layers).
Use this whenever the ask is "every time / whenever / always when , do X" about a LOCAL, in-session behavior.
Quick start โ hook vs the other cadence mechanisms
| Mechanism | Fires when | Machine on? | Session open? | Builder |
|---|
| Hook | a Claude Code EVENT matches | yes | a CC process must exist (no human needed) | this skill |
Scheduled routine (cloud or local cron/launchd + claude -p) | a clock schedule | depends | no | /routines-builder |
/loop | recurring interval inside one session | yes | yes | /loop itself |
| Skill-scoped hook | only while a specific skill runs | yes | yes | /skill-builder (frontmatter) |
| Plugin hook | shipped inside a plugin, fires for its users | โ | โ | /plugin-builder |
Scope (v1): hooks this skill authors are type:"command" only. Claude Code reportedly also supports http/mcp_tool/prompt/agent handlers and fields like if/async โ but these handler names are candidate/observed, not all guaranteed in your build, so confirm each against the official docs first. (prompt/agent handlers also spend tokens on every match, so they need an explicit cost case.) See reference.md before reaching for them.
Division of labor with update-config: where that built-in skill is available, it owns the mechanical settings.json edit. This skill owns everything around it โ the decision, the design, the script, the test discipline โ and invokes update-config for the write (or edits directly when it's unavailable, reporting exactly what changed).
Mode 1: Build
Step 0 โ The Decision Gate (FIRST)
- Is it event-driven inside Claude Code sessions? (a tool call, session start/end, a stop) If it's clock-driven โ
/routines-builder. STOP and refer.
- Must it fire every time, deterministically? If "usually / when relevant" โ it's an instruction for CLAUDE.md or a skill, not a hook. STOP and refer.
- Only while one skill runs? โ skill-scoped hooks in that skill's frontmatter via
/skill-builder. STOP and refer.
- Shipping to others? โ plugin
hooks/hooks.json via /plugin-builder. STOP and refer.
- Deterministic, no LLM judgment needed in the action? Hooks should run scripts, not think. If the action needs judgment, the hook may still fire
claude -p โ but flag the cost and consider whether a skill ritual fits better. A hook that spawns claude -p MUST cap it (--max-turns + a hard timeout) AND carry a re-entry guard (e.g. [ -n "${CLAUDE_HOOK_DEPTH:-}" ] && exit 0; export CLAUDE_HOOK_DEPTH=1 at the top), and must NEVER fire from a Stop hook the same run can re-trigger โ an LLM-spawning hook whose child can re-fire it, with no depth guard, is a fork bomb. See references/agent-loops.md.
State the verdict in one sentence before interviewing.
Step 1 โ Discovery Interview
AskUserQuestion, one round at a time; skip what's known. Why each matters: wrong event = never fires; wrong matcher = fires constantly; wrong failure mode = blocked sessions.
- Round A โ Trigger. Which event? Core set:
SessionStart, UserPromptSubmit, PreToolUse, PostToolUse, Stop (end of each RESPONSE, not session), SubagentStop, SessionEnd, PreCompact โ extended events in reference.md. Which matcher (for tool events: tool-name regex, e.g. Write|Edit; several non-tool events also accept matchers โ see reference.md per-event table)? How often will that realistically fire per session (every-Read hooks run hundreds of times โ keep them <100ms)?
- Round B โ Action. What does it DO: allow/block (exit 2 = block), inject context (stdout on certain events), or side effect (notify, log, format, validate)? Inline command or script file? (Anything >1 line โ a script in
.claude/hooks/ (project) or ~/.claude/hooks/ (user).)
- Round C โ Scope + failure. Scope: user (
~/.claude/settings.json, all projects), project-shared (.claude/settings.json, committed), project-personal (.claude/settings.local.json)? Timeout (default 5-10s โ ALWAYS set one)? On script failure: fail-open (log, continue) or fail-closed (block)? Default fail-open unless it's a guard.
- Confirmation โ echo a fenced
## Hook Summary (event, matcher, action, scope, timeout, failure mode, script path) and get explicit yes.
Step 2 โ Build
- Write the script (if any) to the scoped hooks dir;
chmod +x. Script contract: read the event JSON from stdin, do ONE thing, exit 0 (allow / success) or 2 (block, PreToolUse) โ full protocol in reference.md. Never put secrets in the command line; source them inside the script from .env.
- Config edit: invoke the
update-config skill with the exact hooks JSON block (shape in reference.md). If editing directly, show the diff of the settings file.
Step 3 โ Supervised first-fire test (MANDATORY โ don't skip, don't leave unverified)
Hooks run as YOU with your permissions on every matched event. Before calling it done:
- Dry-fire the script standalone:
echo '<realistic event JSON>' | <script> โ verify output + exit code for both the match case and a benign case.
- Live-fire once: trigger the real event in-session (e.g. a harmless Write for a
PreToolUse:Write hook) and confirm: fired? right decision? no latency pain?
- Failure drill: make the script fail (or time out) once; confirm the session degrades the way Round C chose.
- Report exactly what you ran and what happened. A hook that can't pass all three stays UNARMED (config commented out / removed).
Step 4 โ Document & register
CLAUDE.md one-liner if it changes day-to-day behavior (respect the budget protocol); append references/log.md (## [<date>] create | Hook โ <name>); note in pending.md anything deferred. New hook-layer facts โ update reference.md.
Mode 2: Optimize
Read the current config + script FIRST โ never optimize what you haven't read. Symptoms โ fixes: fires too often โ tighten matcher regex; session feels slow โ measure script runtime, move work async or cache; silent failures โ add logging to a file (never stdout on non-inject events); blocks unexpectedly โ check exit codes (a crashing script can read as exit 2); hook stopped working after an update โ re-verify event names + settings layer precedence.
Mode 3: Audit
For every hook across ~/.claude/settings.json, .claude/settings.json, .claude/settings.local.json, and active plugins:
Complete example (flagship) โ a fail-closed PreToolUse guard
Goal: block destructive shell before it runs. Defensive, fail-closed, fast โ the shape to copy first.
.claude/hooks/bash-guard.sh:
#!/usr/bin/env bash
set -euo pipefail
trap 'echo "bash-guard error โ denying" >&2; exit 2' ERR
cmd="$(jq -r '.tool_input.command // ""')"
case "$cmd" in
*"rm -rf /"*|*"rm -rf ~"*|*"mkfs"*|*"dd if="*)
echo "blocked: destructive command pattern" >&2; exit 2 ;;
esac
exit 0
.claude/settings.local.json fragment (via update-config):
{ "hooks": { "PreToolUse": [ { "matcher": "Bash", "hooks": [ { "type": "command",
"command": "bash \"$CLAUDE_PROJECT_DIR/.claude/hooks/bash-guard.sh\"", "timeout": 5 } ] } ] } }
Test all three: block case echo '{"tool_input":{"command":"rm -rf /"}}' | bash .claude/hooks/bash-guard.sh; echo $? โ message on stderr, exit 2 (denied); benign case echo '{"tool_input":{"command":"ls -la"}}' | bash .claude/hooks/bash-guard.sh; echo $? โ exit 0 (allowed); failure drill echo 'not json' | bash .claude/hooks/bash-guard.sh; echo $? โ trap fires โ exit 2 (fail-CLOSED deny). Then live-fire: ask for a harmless ls (runs) and confirm a destructive command is blocked.
Side-effect counterpart โ a fail-OPEN SessionEnd ping
When the hook is a notification, not a guard, fail-OPEN is right (a failed ping must never wedge the session). SessionEnd โ NOT Stop, which fires at the end of every response.
.claude/hooks/session-end-ping.sh:
#!/usr/bin/env bash
set -a; source "$CLAUDE_PROJECT_DIR/.env" 2>/dev/null; set +a
curl -sm 4 "https://api.telegram.org/bot${TG_BOT_TOKEN}/sendMessage" \
-d chat_id="${TG_CHAT_ID}" -d text="๐ AIOS session ended" >/dev/null || true
exit 0
.claude/settings.local.json fragment (via update-config):
{ "hooks": { "SessionEnd": [ { "hooks": [ { "type": "command",
"command": "bash \"$CLAUDE_PROJECT_DIR/.claude/hooks/session-end-ping.sh\"", "timeout": 6 } ] } ] } }
Test: echo '{}' | bash .claude/hooks/session-end-ping.sh; echo $? โ message arrives, exit 0. Then end one disposable session live. Failure drill: run with FAIL_TEST=1 injected (add [ "${FAIL_TEST:-}" = 1 ] && exit 1 at the top during testing) in a disposable session โ session unaffected (fail-open) โ remove the test line.
Important notes
- The decision gate is not optional โ most "automate X" asks are routines or skills, not hooks.
- Hooks are the only layer that can guarantee "always" โ but each one runs forever on every match. Bias toward FEW, fast, well-tested hooks.
- Read before optimizing; test before arming; route review of any non-trivial hook script to a different-lineage model (No-Self-Review Law in
multi-brain).
- Full schema/protocol: reference.md. Official docs: https://code.claude.com/docs/en/hooks
Related
- reference.md โ events, settings schema, stdin/exit protocol, inventory how-to.
/routines-builder โ clock-driven cadence (hands event-driven work here).
update-config โ the mechanical settings.json writer this skill delegates to (when available).
/skill-builder (skill-scoped hooks) ยท /plugin-builder (plugin hooks).