Hook Chain

Evidence Measured October 12, 2026 · Claude Code 2.1.296

The latency ladder

A hook's own code is almost free. What a hook waits for is where the time goes. With one pass-through hook as 1×, here is the climb from a hook to a file read, a process, a decision model, a language model and a person.

One hook, normalized

One pass-through tool.call hook added about 42 µs per call in the test kit. That's our 1×. The bars use a logarithmic scale from 0.5× to 10,000,000×, so equal lengths mean equal multiples. Without the log scale, every rung below the decision models would be invisible.

Pass-through hook1×
$.env.get≈1×
$.store.get≈2×
$.fs.read, small file≈4×
$.http.fetch, localhost≈14×
$.process.run≈36×
OpenAI Decisions API~7,100×
Jev (hosted)~14,000×
Claude Haiku 5.5, low~60,000×
Hook budget, 10 s240,000×
A person, ~1 minute~1.4M×
Hooks and $ calls (measured here)Decision models (measured by Decision Tier)LLM (measured by Decision Tier) and the budget lineHuman (assumption)

Where each number comes from

RungBasisMedianMultiple
Pass-through hookTest kit: 1 hook against 0 hooks, 3 × 2,000 calls, 3 separate runs~42 µs1×
$.env.getLive headless session, 200 calls, 4 runs~43 µs≈1×
$.store.getLive, 200 calls, 4 runs~90 µs≈2×
$.fs.read (small file)Live, 50 calls, 4 runs~180 µs≈4×
$.http.fetch to a local serverLive, 20 calls, 3 runs, Python http.server on 127.0.0.1~0.58 ms≈14×
$.process.run(['true'])Live, 20 calls, 4 runs~1.5 ms≈36×
OpenAI Decisions APIAs measured by Decision Tier; p95 0.76 s0.30 s~7,100×
Jev jev-1.13.0, hostedAs measured by Decision Tier, 13-question requests, 4 in flight; p95 2.09 s0.59 s~14,000×
Claude Haiku 5.5, effort lowAs measured by Decision Tier; p50 2–3 s, p95 3.5–4.8 s~2.5 s~60,000×
A hook's own-time budgetMods reference, Limits; confirmed live in Field tests10 s240,000×
A person answering $.ui.askAssumption, not measured: they have to notice, read and choose~1 min~1.4M×

The hook and $ rows come from our experiments: latency/ in the test kit and live-probe/ in a real headless session, which drops its prompt so no model is called. The kit row is the chain's overhead in the kit's engine, with no session or terminal drawing. A live interactive session does more work around each call, not less. The decision-model and LLM rows are network round trips measured on another project's workloads, so take them as orders of magnitude for your hook, not as promises.

The chain itself barely costs anything

We stacked pass-through hooks by matcher, so the same module gave four chain depths, and then repeated the test with the hooks spread across separate mods. Per-call medians from the test kit, one of three runs that agreed to within a few µs:

ConfigurationPer callOver no hooks
No hooks (the kit's own dispatch)43 µs—
1 hook85 µs+42 µs
5 hooks in one module80 µs+37 µs
20 hooks in one module81 µs+38 µs
1 hook that makes one $ call131 µs+88 µs
1 separate mod90 µs+47 µs
5 separate mods114 µs+71 µs
20 separate mods334 µs+291 µs

Three things stand out. Entering a mod costs about 42 µs. More hooks inside the same module cost nothing we could measure: 20 measured no slower than 1. Each additional separate mod adds about 13 µs on average. Twenty mods on every tool call add about a third of a millisecond, while a tool call that reads a file takes far longer. You won't feel the chain.

What this means for design

Spend the budget on waiting, not on thinking

A hook has 10 seconds of its own time per event. In the reference's words, "the clock stops while a next(e) call or any $ call of the hook's is in flight (a $.clock wait excepted), so a slow chain beneath or a minute-long $.model.complete costs it nothing." A guard that sends a command to a decision model through $.http.fetch and waits 0.6 seconds for the answer has spent almost none of its budget. A guard that loops in JavaScript for 10 seconds, or polls with $.clock.sleep, has spent all of it. When that happens it's skipped, and the call it was guarding goes ahead (see Field tests, section 2).

Every rung up is a bigger bet

Two of these are still tiny: a regex on e.command (well under 1×) and a $.store lookup (2×). Asking a decision model is four orders of magnitude slower. Asking Claude is another factor of four or so. Asking the person is slower again by a factor of 25. That's the same shape as Decision Tier's cost ladder, measured in time instead of money. The design rule is the same too: answer in the cheapest rung that can answer correctly, and climb only for what's left. Mods meet the cascade builds a guard that way.

Hold, don't spin

A guard that needs a human holds the call with $.ui.ask. That's a $ call, so the wait doesn't count against the budget, and the person can take their time. The events page puts it plainly: "Time spent awaiting a promise of your own does count. Claude Code skips a hook that times out, so the held command would run." Two more details. $.ui.ask rejects when the person dismisses the question, and always in a claude -p run, so wrap it in try and treat a rejection as a refusal. And give the hook a .catch that refuses. The handler has its own 1-second grace period, which is enough for { deny } and not enough for anything clever.

Redraws have their own speed limit

$.ui.invalidate('ui.render') is throttled to 10 redraws a second, or 30 for visible terminal sites. A spinner suffix that updates on every tool call is fine. A pane that tries to animate at 60 fps isn't. Write to $.state and let the engine redraw only the sites that read it.

The deadlines that aren't 10 s

LimitValueDesign consequence
A hook's own time per event10 sAwait $ calls freely. Don't compute or sleep for long.
A .catch handler's grace1 sDecide from e and next.called alone. Refuse rather than retry.
All session.end hooks, together1.5 sSave as you go, not at exit. Several mods share these 1.5 seconds.
Redraw throttle10/s (30/s visible terminal)Use reactive $.state, not invalidate loops.
A test in claude plugin test5 sThe budget can't be tested in the kit. Check timeouts in a headless session.

See also: The hook chain for where in the chain these costs land, and Mods meet the cascade for a guard that climbs this ladder only when it has to.