Hook authoring standards
A hook is a shell command Claude Code runs automatically in response to a lifecycle event. Hooks wire the harness to your conventions deterministically — the model can’t forget, skip, or be talked around a hook. Hooks ship standalone at hooks/<name>/hooks.json (with a sibling meta.json) or bundled at plugins/<plugin>/hooks/hooks.json.
This page is two layers: Anthropic’s official guidance, then the RBA tailoring on top.
Anthropic’s best practices
Section titled “Anthropic’s best practices”Source: Claude Code — Hooks reference and Get started with hooks.
Events
Section titled “Events”Hooks fire on these lifecycle events:
PreToolUse— before a tool runs. Can block the tool.PostToolUse— after a tool succeeds.UserPromptSubmit— when the user submits a prompt. Can inject context or block.Notification— when Claude Code sends a notification (e.g., permission prompt).Stop— when the main agent finishes responding.SubagentStop— when a subagent finishes.PreCompact— before context compaction.SessionStart/SessionEnd— at session boundaries.
Include only the events your hook actually fires on.
The input/output contract
Section titled “The input/output contract”- Input: Claude Code passes structured JSON on stdin (session id, event name, tool name, tool input, etc.). Parse stdin; don’t rely on argv ordering.
- Exit codes:
0= success (stdout shown to the user in transcript mode; forUserPromptSubmit/SessionStart, stdout is added to context).2= blocking error — stderr is fed back to Claude and the action is blocked (where the event supports blocking). Any other non-zero = non-blocking error; stderr is shown to the user, execution continues. - Structured output (advanced): a hook can print a JSON object on stdout to control behavior precisely —
continue,decision/permissionDecision,reason,additionalContext,suppressOutput. This is the preferred mechanism when exit codes are too blunt.
Matchers
Section titled “Matchers”PreToolUse/PostToolUse entries take a matcher against the tool name (exact string or regex, e.g. Edit|Write, Bash, mcp__.*). Other events don’t match on a tool. Anthropic recommends matching as narrowly as possible.
Configuration shape
Section titled “Configuration shape”Hooks are configured in settings (settings.json hooks block) or, for plugins, a hooks.json:
{ "PreToolUse": [ { "matcher": "Write|Edit", "hooks": [ { "type": "command", "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/guard.sh" } ] } ]}Anthropic’s general advice: keep hooks fast and deterministic, validate and quote inputs, use absolute paths, and never trust hook input blindly (it can contain user-controlled content).
How RBA tailors this
Section titled “How RBA tailors this”When to write a hook at all (RBA decision rule)
Section titled “When to write a hook at all (RBA decision rule)”A hook is the right tool when you need a guarantee, not a guideline.
| Reach for a hook when… | Use CLAUDE.md / a skill instead when… |
|---|---|
| The rule must hold every time, not just when remembered | The rule is a preference or style point |
| It can be checked deterministically by a script | The rule requires judgment |
| Failure should block / surface loudly | Failure is a soft warning |
| Compliance, security, or audit context | Tone, code style, taste |
Standalone meta.json sidecar (RBA)
Section titled “Standalone meta.json sidecar (RBA)”A standalone hook lives in hooks/<name>/ with two files:
hooks/<name>/├── hooks.json # Anthropic hook config (the shape above)└── meta.json # RBA catalog metadata{ "name": "<name>", "description": "<event + what it does>", "primitive": true, "platforms": [], "disciplines": [], "lifecycle": [], "tags": [], "outcomes": []}primitive is optional — set it to true only for cross-cutting foundation hooks. Plugin-bundled hooks inherit primitive from the plugin’s rba-meta.json and must not redeclare it. See plugin standards for the full facet model.
RBA design rules (on top of Anthropic’s contract)
Section titled “RBA design rules (on top of Anthropic’s contract)”- Fail loud, fail fixable. Exit
2with an actionable message (Refusing to write outside the project directory: <path>) rather than mutating silently. This operationalizes Anthropic’s exit-code contract. - Idempotent. Running twice equals running once — no append-without-dedup, no double-notify.
- Fast. Hooks fire on every matched event; aim for well under one second. Slow work goes async.
- Self-contained. No dependency on uncommitted local state; only on the bundle and the user’s standard environment.
- Read stdin, not argv (Anthropic’s contract — RBA enforces it in review).
- stdout/stderr discipline per Anthropic’s exit-code semantics above.
- Narrow matchers. A catch-all
.*matcher where a specific one would do is an anti-pattern.
Documentation requirement (RBA)
Section titled “Documentation requirement (RBA)”The plugin README (or standalone meta.json + catalog page) must, for each hook, state: the event, the matcher, what it does and why, side effects (files written, commands run, network calls), and how to disable it.
Quality checklist
Section titled “Quality checklist”- The hook reads JSON from stdin and uses documented exit codes (Anthropic contract).
- Matchers are as narrow as possible (Anthropic).
- The hook was reviewed for what it executes — no untrusted input run unquoted (Anthropic security).
- It’s idempotent and completes in under a second (RBA).
- It fails loud with an actionable message and exit
2where blocking is intended (RBA). - Standalone hooks have
meta.jsonwithname,description, and at least one populated facet (orprimitive: true) (RBA). - Documentation covers event, matcher, behavior, side effects, and disable path (RBA).
- The hook does not write outside the project directory without explicit confirmation.
Anti-patterns
Section titled “Anti-patterns”- A hook that runs longer than the action it’s hooking
- A hook that mutates state silently with no log (violates Anthropic’s transparency intent)
- A hook that runs untrusted stdin content unquoted (Anthropic security warning)
- Catch-all matcher when a specific matcher would do
- A hook that swallows errors and exits
0 - A network-dependent hook that blocks indefinitely on connection failure