Skip to content
Beta Multi-model support (OpenAI, Gemini, and more) launching soon.

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.

Source: Claude Code — Hooks reference and Get started with hooks.

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.

  • 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; for UserPromptSubmit/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.

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.

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).

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 rememberedThe rule is a preference or style point
It can be checked deterministically by a scriptThe rule requires judgment
Failure should block / surface loudlyFailure is a soft warning
Compliance, security, or audit contextTone, code style, taste

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)”
  1. Fail loud, fail fixable. Exit 2 with an actionable message (Refusing to write outside the project directory: <path>) rather than mutating silently. This operationalizes Anthropic’s exit-code contract.
  2. Idempotent. Running twice equals running once — no append-without-dedup, no double-notify.
  3. Fast. Hooks fire on every matched event; aim for well under one second. Slow work goes async.
  4. Self-contained. No dependency on uncommitted local state; only on the bundle and the user’s standard environment.
  5. Read stdin, not argv (Anthropic’s contract — RBA enforces it in review).
  6. stdout/stderr discipline per Anthropic’s exit-code semantics above.
  7. Narrow matchers. A catch-all .* matcher where a specific one would do is an anti-pattern.

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.

  1. The hook reads JSON from stdin and uses documented exit codes (Anthropic contract).
  2. Matchers are as narrow as possible (Anthropic).
  3. The hook was reviewed for what it executes — no untrusted input run unquoted (Anthropic security).
  4. It’s idempotent and completes in under a second (RBA).
  5. It fails loud with an actionable message and exit 2 where blocking is intended (RBA).
  6. Standalone hooks have meta.json with name, description, and at least one populated facet (or primitive: true) (RBA).
  7. Documentation covers event, matcher, behavior, side effects, and disable path (RBA).
  8. The hook does not write outside the project directory without explicit confirmation.
  • 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