Hooks builder
Skill GerardoRdz96/aios-starter-kit/.claude/skills/hooks-builder
Build your own personal AI Operating System — a clonable Claude Code template with onboarding, a self-auditing health check, builder skills, and a Karpathy-style knowledge wiki. Includes a bilingual class teaching package.
npx -y skills add GerardoRdz96/aios-starter-kit --skill hooks-builderAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 1 stars1 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
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).
SKILL.md
11.6 KB, as published. Nobody here has run it
/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 <event>, 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.jsonvia/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 spawnsclaude -pMUST cap it (--max-turns+ a hardtimeout) AND carry a re-entry guard (e.g.[ -n "${CLAUDE_HOOK_DEPTH:-}" ] && exit 0; export CLAUDE_HOOK_DEPTH=1at the top), and must NEVER fire from aStophook the same run can re-trigger — an LLM-spawning hook whose child can re-fire it, with no depth guard, is a fork bomb. Seereferences/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-configskill 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:Writehook) 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:
- Event + matcher still match real tool names (no stale regexes)
- Timeout set; script exists, executable, <100ms for high-frequency events
- Failure mode intentional (fail-open vs fail-closed) and documented
- No secrets in command lines; scripts source
.envinternally - Still wanted — kill zombie hooks (each one taxes every matched event forever)
- Layer is right (user vs project vs local) for who should get it
- Registered: log entry exists; CLAUDE.md mentions it if behavior-changing
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
# PreToolUse:Bash guard — fail-CLOSED. Blocks dangerous commands; denies on its own errors.
set -euo pipefail
trap 'echo "bash-guard error — denying" >&2; exit 2' ERR
cmd="$(jq -r '.tool_input.command // ""')" # the command the model wants to run
# Decision logic in a conditional (exempt from set -e): a match blocks with exit 2.
case "$cmd" in
*"rm -rf /"*|*"rm -rf ~"*|*"mkfs"*|*"dd if="*)
echo "blocked: destructive command pattern" >&2; exit 2 ;;
esac
exit 0 # default: allow
.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
# SessionEnd ping; fail-open. Reads event JSON on stdin (unused).
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).