Skip to content

Lead · 9 chapters

Hooks and deterministic interception

Enforcing an invariant in code at a lifecycle point rather than asking the model to remember it

Prompt-only invariants eventually fail under model variation, while poorly placed hooks create hidden latency, ordering bugs, and unsafe partial enforcement.

Read these first

What you will know

  • Enforce an invariant with a hook rather than an instruction

    The check must execute on every applicable path, including model retries and tool failures, without relying on the model to cooperate.

  • Write a hook that rejects or repairs a violating event at a chosen lifecycle boundary

    A rejection should preserve a safe failure path, while repair logic must not silently change the requested operation.

  • Trace hook ordering and execution context across a complete agent run

    Ordering determines whether the hook sees original input, rewritten state, tool results, or already-committed side effects.

  • Break a hook with retries, nested calls, and partial failures, then patch it to be idempotent

    Repeated delivery and interrupted execution can otherwise duplicate writes, emit conflicting decisions, or leave state half-enforced.

  • Run the same agent action with and without the hook and record invariant violations, added latency, and failure behavior

    The comparison exposes whether deterministic enforcement fixes the target problem without turning normal traffic into a bottleneck or opaque error source.


Chapters

  1. 1

    A hook runs whether or not the model cooperates

    "Never run terraform apply without a plan attached" sits at about 90% compliance as an instruction, and the missing 10% is not fixed by better wording. Moving it into code at a lifecycle point makes the outcome guaranteed rather than likely. The subtlety is that the hook teaches the agent nothing, so deleting the instruction leaves it constructing rejected applies and inventing workarounds — guidance makes the happy path likely, the hook makes the bad path impossible.

  2. 2

    Lifecycle position decides control versus detection

    Register the infra/prod/ write guard after the tool call completes and it will fire correctly, log every violation, and prevent nothing — the write already happened, so all that remains is paging someone and attempting rollback. Tracing a full agent run shows what each boundary can see: original arguments, rewritten state, tool results, or committed side effects. Where you attach determines whether you built a control or an alarm.

  3. 3

    Rejecting is safe, silently substituting is not

    A hook that sees delete_records without a dry_run flag and adds one lets the agent report deletions that never happened, and operations find the records weeks later. Repair changed the semantics of the requested operation while reporting success, so the agent's model of the world and the world diverged. A hook may refuse an unsafe call, or normalise something the caller demonstrably does not depend on, but never quietly perform a different operation. The refusal is itself a design decision: a rejection that names the rule and the permitted alternative puts the agent on a different next action than an opaque denial, which mostly invites the same call again.

  4. 4

    Retries turn one attempt into several hook firings

    A pre-call hook writing an audit row per transfer_funds attempt looks correct until the tool client gains retries: auditors find duplicate rows for single transfers and one row with no transfer behind it. Key the write on a stable identifier for the attempt so repeated delivery collapses, and record the outcome separately, because a pre-call hook can only ever assert that a call was made. The same reasoning covers nested calls and interrupted execution.

  5. 5

    A hook enforces only what a machine can decide

    Asked to block any action breaching EU data residency, the tempting design inspects outbound tool arguments and classifies whether the payload contains EU personal data. That classification is a judgement, so the hook either calls a model — putting probabilistic reasoning inside the mechanism whose value was determinism — or pattern-matches and misses anything unlabelled. Distinguishes decidable predicates like destination and path from undecidable ones like whether a change is well designed.

  6. 6

    An agent routes around a boundary drawn at the wrong layer

    A hook blocking writes outside the working tree works for two weeks, until the agent starts base64-encoding contents into a shell command that reconstructs the file elsewhere. Patching for base64 treats a structural problem as a bug list: a heredoc, a Python one-liner, a temp file plus a move all follow. The agent is not adversarial, it is routing toward the goal you gave it, which tells you the boundary belongs where the effect is, not where one syntax is.

  7. 7

    Measure the hook's latency and its failure direction

    Run the same agent action with and without the hook and record three things: invariant violations, added latency on normal traffic, and what happens when the hook's own dependency is down. The result that should stop a release is calls proceeding unchecked during that window, because the invariant then holds only while a second system is healthy. Opaque errors on ordinary requests are the other outcome worth catching before shipping.

  8. 8

    Probe the composed behaviour, not the thirty hooks

    Reading thirty implementations tells you what each hook was meant to do, and the moment two of them contradict each other on one path the components stop adding up to the system. The method that survives this is empirical: write down the invariants you believe hold, fire a violating action for each one down the real path, and record what was blocked, what was silently rewritten, and what went through untouched. The same probe is how you find the paths a hook never covered at all — a second client that bypasses the wrapped one, a predicate that does not match the tool it was written for, a nested call that never reaches the intercepted boundary.

  9. 9

    Thirty hooks with no statement of what they enforce

    Two years of additions take median tool-call latency from 40 ms to 260 ms, one hook rewrites an argument another then rejects, and nobody can name the invariants that hold end to end. Consolidating into a single policy engine moves the contradictions into rule ordering and pays the latency inside one process. What was missing architecturally is a registry of invariants and the boundary each is decided at, plus what that discipline makes harder.

External resources