hook-authoring
<!-- Loaded by foundry:sw-engineer (opus + xhigh) -->
$ npx -y skills add Borda/AI-Rig --agent claude-codeHow it fires
How this agent gets triggered: by you, by Claude, or both.
- Fires itselfAuto-invocation. Claude auto-loads it when your prompt matches the work.Auto-invocation is when the right skill fires by itself at the right moment, driven by a FLOW.md router and a hook, instead of you invoking it by name. It is the difference between a skill being installed and a skill actually getting used.Read the full definition →
- You can call itInvoke it directly when you want it.
Context preview
The summary Claude sees to decide when to auto-load this agent.
<!-- Loaded by foundry:sw-engineer (opus + xhigh) -->
Agent definition
hook-authoring.md<!-- Loaded by foundry:sw-engineer (opus + xhigh) -->
Hook Authoring (foundry:sw-engineer specialized guidance)
Read only when working on hook code (JavaScript files under `.claude/hooks/`, hook registrations in `settings.json`, `PostToolUse`/`PreToolUse`/`SubagentStop` event handlers). Skip for Python implementation tasks.
Hook authoring and editing owned exclusively by `foundry:sw-engineer` (per curator NOT-for boundary — curator does not touch hook files). `foundry:curator` reviews hook-adjacent markdown config files only. For hook creation or modification, `foundry:sw-engineer` owns work end-to-end.
File Header Structure
Every hook file must start with:
#!/usr/bin/env node
// <filename>.js — <HookType> hook ← the word `hook` is literal, not a placeholder
//
// PURPOSE
// <one-paragraph description of what this hook does and why>
//
// HOW IT WORKS
// 1. <step>
// 2. <step>
// ...
//
// EXIT CODES
// 0 <success case>
// 1 <error case — blocking hooks: blocks Claude execution; non-blocking hooks: logged as error only, does NOT block>
// 2 <feedback case — Claude Code shows output and Claude acts on it>
Subsection order: `PURPOSE` → `HOW IT WORKS` → `EXIT CODES` (add others like `HOOK EVENT RESPONSIBILITIES` as needed). `HOW IT WORKS` may not be omitted even for simple hooks — use at least one numbered step.
Minimal exit-code template (always pair success + error paths)
# success path
exit 0
# error path
echo "Error: <message>" >&2
exit 1
Every hook must explicitly handle error path — never leave it implicit. For blocking hooks (`exit 1` = block), message on stderr surfaces to user; for non-blocking hooks, logged but does not stop execution.
Exit Code Rules
Exit 1 behavior depends on hook type — do not use exit 1 uniformly:
| Hook type | exit 0 | exit 1 | exit 2 | | --- | --- | --- | --- | | **Blocking** (UserPromptSubmit, PreToolUse gatekeeper, PreCompact) | allow / proceed | **blocks Claude execution** | show output to Claude; Claude acts on it | | **Non-blocking** (PostToolUse, Stop, SubagentStop, SubagentStart, observational PreToolUse) | proceed | logged as error only — does **NOT** block execution | show output to Claude; Claude acts on it |
- **Blocking hooks** (those that emit `permissionDecision`): exit 1 to block; exit 0 to allow
- **Non-blocking/logging hooks**: exit 0 always on unexpected errors — exit 1 logged but does not stop Claude; hooks must never crash or interfere with execution due to hook bug
- **Exit 2 to surface feedback** — Claude Code shows exit-2 output to Claude, which acts on it
- **Exit 2 only when Claude caused condition and can fix it** (e.g. file it wrote failed linting). Use exit 0 for environmental conditions: missing tools, missing config files, unexpected input formats.
Implementation Pattern
- CommonJS: `require()` imports, stdin JSON parse, `process.exit()`
- **Only permitted stdin pattern** — use event-based accumulation; do not use `fs.readFileSync("/dev/stdin")` or any synchronous stdin read:
let raw = "";
process.stdin.setEncoding("utf8");
process.stdin.on("data", (d) => (raw += d));
process.stdin.on("end", () => {
const data = JSON.parse(raw);
// ... handler logic
});- Wrap all logic in try/catch; catch behaviour depends on hook type:
- **PreToolUse gatekeeper hooks** (those that emit `permissionDecision`): catch → `process.exit(1)` — erroring gatekeeper must **block**, not allow. Mapping: `exit(1)` = blocked, `exit(0)` = allowed. Allowing a tool call when gate logic crashed is a security bypass.
- **Logging hooks** (PostToolUse, SubagentStop, observational PreToolUse without decision output): catch → `process.exit(0)` — silent-swallow acceptable; logging hooks must not interfere with Claude's execution.
- Use `execFileSync` or `spawnSync` (not `execSync` with shell strings) for subprocess calls — both take args array, avoiding shell injection. Use `execFileSync` when command MUST succeed (throws on non-zero exit, use in try/catch). Use `spawnSync` when need to inspect result code (returns `{status, stdout, stderr}`, does not throw). **Always pass `{ maxBuffer: 10 * 1024 * 1024 }` (10 MB) to `execFileSync`** — default buffer is 1 MB; subprocess producing unbounded output (e.g. large lint run) will hang or crash Claude Code session without this cap.
PreToolUse Decision Output
When `PreToolUse` hook needs to approve or block tool call, use `hookSpecificOutput` (current format):
{
"hookSpecificOutput": {
"permissionDecision": "allow",
"permissionDecisionReason": "optional explanation shown to user"
}
}- `permissionDecision`: `"allow"` or `"block"` — use `"block"` to prevent tool call
- **Deprecated**: top-level `"decision"` and `"reason"` fields — still work but may be removed in future Claude Code release; migrate to `hookSpecificOutput`
- Most hooks need no decision output — emit only when hook acts as gatekeeper
PostToolUse and SubagentStop Hooks
Logging hooks (timing, file-writes, audit trails) need no output — exit 0 silently. Never emit to stdout from logging hook; unexpected output can interfere with Claude's tool-result handling.
- `PostToolUse` receives tool result payload on stdin — use for timing deltas, logging tool output size, or writing audit records
- `SubagentStop` fires when spawned agent completes — use to clean up per-agent state files (e.g. `${TMPDIR:-/tmp}/claude-state-<session-id>/agents/<id>.json`) (harness-managed path: `${TMPDIR:-/tmp}/claude-state-<session-id>`; not user-configurable) <!-- tmpdir-exempt: harness-managed path, not plugin-authored -->
- Both hook types: wrap all logic in try/catch; catch → `process.exit(0)` always
Anti-patterns
- **Prohibited**: `execSync` with shell string — shell injection risk; takes raw string parsed by `/bin/sh`. Use `execFileSync(cmd, argsArray)` or `spawnSync(cmd, argsArray)` instead.
Read more
<!-- Loaded by foundry:sw-engineer (opus + xhigh) -->
Hook Authoring (foundry:sw-engineer specialized guidance)
Read only when working on hook code (JavaScript files under `.claude/hooks/`, hook registrations in `settings.json`, `PostToolUse`/`PreToolUse`/`SubagentStop` event handlers). Skip for Python implementation tasks.
Hook authoring and editing owned exclusively by `foundry:sw-engineer` (per curator NOT-for boundary — curator does not touch hook files). `foundry:curator` reviews hook-adjacent markdown config files only. For hook creation or modification, `foundry:sw-engineer` owns work end-to-end.
File Header Structure
Every hook file must start with:
#!/usr/bin/env node // <filename>.js — <HookType> hook ← the word `hook` is literal, not a placeholder // // PURPOSE // <one-paragraph description of what this hook does and why> // // HOW IT WORKS // 1. <step> // 2. <step> // ... // // EXIT CODES // 0 <success case> // 1 <error case — blocking hooks: blocks Claude execution; non-blocking hooks: logged as error only, does NOT block> // 2 <feedback case — Claude Code shows output and Claude acts on it>
Subsection order: `PURPOSE` → `HOW IT WORKS` → `EXIT CODES` (add others like `HOOK EVENT RESPONSIBILITIES` as needed). `HOW IT WORKS` may not be omitted even for simple hooks — use at least one numbered step.
Minimal exit-code template (always pair success + error paths)
# success path exit 0 # error path echo "Error: <message>" >&2 exit 1
Every hook must explicitly handle error path — never leave it implicit. For blocking hooks (`exit 1` = block), message on stderr surfaces to user; for non-blocking hooks, logged but does not stop execution.
Exit Code Rules
Exit 1 behavior depends on hook type — do not use exit 1 uniformly:
| Hook type | exit 0 | exit 1 | exit 2 | | --- | --- | --- | --- | | **Blocking** (UserPromptSubmit, PreToolUse gatekeeper, PreCompact) | allow / proceed | **blocks Claude execution** | show output to Claude; Claude acts on it | | **Non-blocking** (PostToolUse, Stop, SubagentStop, SubagentStart, observational PreToolUse) | proceed | logged as error only — does **NOT** block execution | show output to Claude; Claude acts on it |
- **Blocking hooks** (those that emit `permissionDecision`): exit 1 to block; exit 0 to allow
- **Non-blocking/logging hooks**: exit 0 always on unexpected errors — exit 1 logged but does not stop Claude; hooks must never crash or interfere with execution due to hook bug
- **Exit 2 to surface feedback** — Claude Code shows exit-2 output to Claude, which acts on it
- **Exit 2 only when Claude caused condition and can fix it** (e.g. file it wrote failed linting). Use exit 0 for environmental conditions: missing tools, missing config files, unexpected input formats.
Implementation Pattern
- CommonJS: `require()` imports, stdin JSON parse, `process.exit()`
- **Only permitted stdin pattern** — use event-based accumulation; do not use `fs.readFileSync("/dev/stdin")` or any synchronous stdin read:
let raw = "";
process.stdin.setEncoding("utf8");
process.stdin.on("data", (d) => (raw += d));
process.stdin.on("end", () => {
const data = JSON.parse(raw);
// ... handler logic
});- Wrap all logic in try/catch; catch behaviour depends on hook type:
- **PreToolUse gatekeeper hooks** (those that emit `permissionDecision`): catch → `process.exit(1)` — erroring gatekeeper must **block**, not allow. Mapping: `exit(1)` = blocked, `exit(0)` = allowed. Allowing a tool call when gate logic crashed is a security bypass.
- **Logging hooks** (PostToolUse, SubagentStop, observational PreToolUse without decision output): catch → `process.exit(0)` — silent-swallow acceptable; logging hooks must not interfere with Claude's execution.
- Use `execFileSync` or `spawnSync` (not `execSync` with shell strings) for subprocess calls — both take args array, avoiding shell injection. Use `execFileSync` when command MUST succeed (throws on non-zero exit, use in try/catch). Use `spawnSync` when need to inspect result code (returns `{status, stdout, stderr}`, does not throw). **Always pass `{ maxBuffer: 10 * 1024 * 1024 }` (10 MB) to `execFileSync`** — default buffer is 1 MB; subprocess producing unbounded output (e.g. large lint run) will hang or crash Claude Code session without this cap.
PreToolUse Decision Output
When `PreToolUse` hook needs to approve or block tool call, use `hookSpecificOutput` (current format):
{
"hookSpecificOutput": {
"permissionDecision": "allow",
"permissionDecisionReason": "optional explanation shown to user"
}
}- `permissionDecision`: `"allow"` or `"block"` — use `"block"` to prevent tool call
- **Deprecated**: top-level `"decision"` and `"reason"` fields — still work but may be removed in future Claude Code release; migrate to `hookSpecificOutput`
- Most hooks need no decision output — emit only when hook acts as gatekeeper
PostToolUse and SubagentStop Hooks
Logging hooks (timing, file-writes, audit trails) need no output — exit 0 silently. Never emit to stdout from logging hook; unexpected output can interfere with Claude's tool-result handling.
- `PostToolUse` receives tool result payload on stdin — use for timing deltas, logging tool output size, or writing audit records
- `SubagentStop` fires when spawned agent completes — use to clean up per-agent state files (e.g. `${TMPDIR:-/tmp}/claude-state-<session-id>/agents/<id>.json`) (harness-managed path: `${TMPDIR:-/tmp}/claude-state-<session-id>`; not user-configurable) <!-- tmpdir-exempt: harness-managed path, not plugin-authored -->
- Both hook types: wrap all logic in try/catch; catch → `process.exit(0)` always
Anti-patterns
- **Prohibited**: `execSync` with shell string — shell injection risk; takes raw string parsed by `/bin/sh`. Use `execFileSync(cmd, argsArray)` or `spawnSync(cmd, argsArray)` instead.
Specialist-agent infrastructure for Python/ML OSS — the scaffolding that lets you maintain at scale without becoming a full-time reviewer.
Repo: Borda/AI-Rig
Other agents on ai-rig.
- challenger
Adversarial review — drills to bedrock, treats claims as unproven until evidence. NOT for: plan design (foundry:solution-architect), test coverage (foundry:qa-specialist), config formatting (foundry:curator). TRIGGER: "challenge this", "devil''s advocate", "poke holes in". SKIP:
Open agent - creator
Content specialist — blog posts, slide decks, social threads, talk abstracts. Reads approved outline, applies four-beat arc. NOT for in-code docs/README/FAQs (foundry:doc-scribe), release notes (oss:release). TRIGGER: "write a blog post", "create slides", "draft a thread". SKIP:
Open agent - curator
Config quality reviewer. Scope: agents/skills/rules (*.md) — verbosity, duplication, cross-refs, roster overlap; applies fixes. NOT for hooks (foundry:sw-engineer), ADRs (foundry:solution-architect), adversarial challenge (foundry:challenger). TRIGGER: "audit this agent",
Open agent - doc-scribe
Docs specialist — docstrings, API refs, README, standalone FAQ/comparison tables. NOT for CHANGELOG (oss:shepherd), linting (foundry:linting-expert), implementation (foundry:sw-engineer), narrative content (foundry:creator). TRIGGER: "write docs for", "add docstrings to",
Open agent - specialized-patterns
<!-- Loaded by foundry:doc-scribe (sonnet + medium) -->
Open agent - linting-expert
Python static analysis — ruff, mypy, pre-commit, lint/type fixes, type annotations. NOT for CI topology (oss:cicd-steward), test logic (foundry:qa-specialist), non-style implementation (foundry:sw-engineer), docstrings (foundry:doc-scribe). TRIGGER: "is this clean", "lint
Open agent

