Hook Chain

Architecture How an event travels

The hook chain

Every hook for an event joins one chain. The event goes in from the outermost seat, passes inward link by link to Claude Code's own behaviour, and the result comes back out the same way. Where a hook sits decides what it can see, what it can stop and who can overrule it.

Concentric garden walls seen from above, tinted violet, clay, sage and gold from outside in, labelled PREPEND, USER, APPEND and BUILTIN, with a glowing brass engine labelled CORE at the center. A card travels inward through a gate in each ring, and a rolled-up result scroll travels back outward.
Outer rings see the event first and the result last. A ring that answers stops the card from going any further in.

The order

OUTERMOST  sees the event first, the result last
  1. prepend   sec-default@builtin (where it loads), prependPlugins in order, other org mods
  2. user      mods the user installed (a mod runs before the mods it depends on)
  3. append    appendPlugins
  4. builtin   agents-md, diff, telemetry, …
  5. core      Claude Code itself: { plugin: 'engine', tier: 'core' }
INNERMOST
Within one module: the order `on` was called.

A tool call, end to end

The tool path has the most layers, because settings hooks and permission checks interleave with the mod chain:

model emits tool_use managed PreToolUse hooksa block here is final; no mod sees the call mod chain: tool.call prepend · org policy, sec-default user · your installed mods append builtin core: non-managed PreToolUse hookssettings files and plugins' hooks.json tool.check chainnext(e) = rules + mode + hooks verdict → allow | ask | deny permission prompt (no mod can draw here) the tool runs result flowsback up throughtool.call hooks each hook: observe,rewrite, deny, answer,retry, post-process

Try it: a chain simulator

A Bash call: rm -rf build

Choose what the hook in each seat does, then read what happens. This simulates the documented rules; it doesn't run Claude Code.

    Tools an outer hook has

    ToolWhat it does
    next(e) / next({ ...e })Continue inward, unchanged or rewritten. Resolves to the result from everything beneath.
    Return without nextAnswer. Nothing beneath runs.
    await next(e), then editPost-process the result on its way out.
    Call next twiceRetry what's beneath.
    next.to(e, 'append' | 'builtin' | 'core')Prepend and append mods only: skip the user tier.
    next.originWho raised the event. Because every $ call is an event, a prepended hook on fs.read sees which mod asked.
    next.signalAborts when the dispatch is abandoned.
    .catch(handler)Runs if the hook throws, times out or is re-entered. next.called says whether it got as far as next.

    Why order beats parallel

    Settings hooks for the same event run in parallel. When they disagree, the most restrictive decision wins, and when two rewrite a call's input, the last writer wins, in no fixed order. That's fine for allow-or-block and awkward for anything that composes. Mods run in sequence, so a redactor can run before a logger and the logger sees redacted text. A policy mod in prepend sees every user mod's rewrite before core does, and can audit or refuse every user mod's $ calls. The price of order is that a slow hook delays everything inside it, and a failed one is skipped unless it says otherwise.

    Other chains worth knowing

    EventWhat a hook's answer means
    prompt.submitRewrite text (the transcript shows the new text), add Claude-only context, or { drop }.
    prompt.composeThe whole system prompt as { sections: [{ id, text, scope }] }. scope: 'shared' sits before the cache boundary. Text that changes between requests breaks the prompt cache.
    turn.stepAn async generator per model request. yield* next(e) streams through. A hook can change model or effort, rewrite chunks, or answer without calling the model.
    agent.spawnRewrite a subagent's prompt, type, model or working folder, or { deny }. Covers teammates (2.1.289) and workflow agents (2.1.292).
    session.appendRewrite a stored row's content, for example to redact it. A row the engine appends is never refused.
    ui.render on AbovePromptReturning your own tree hides what mods further in drew, unless you embed await next(e).