Skip to content

How the hook decides

A hook is a script the agent’s harness runs before a tool call — it can let the call through, block it, or escalate it to a human. Every generated bundle ships one shared decision core, plugin/hookcore.mjs, that makes that call.

It reads the bundle’s catalog.json — the one list of what’s approved and how each operation is gated — and returns allow, deny, or ask before the call leaves the harness. Thin per-harness shims (Antigravity, Claude Code, Codex) translate that single verdict into each harness’s own PreToolUse format.

decision core · plugin/hookcore.mjs outcome · before the call leaves the harness pretooluse · the model proposes a tool call 1 · known to catalog.json? the committed catalog is the source of truth — not the server no deny not an operation of this bundle tamper / staleness guard — a swapped or stale server is refused known tool 2 · state === approved? the server never compiles an unapproved operation into a tool no deny draft / rejected — not approved for use mirrors the approval filter — unapproved ops never ship approved 3 · confirmation required · human-approval tier? AIR confirmation.humanApproval — the strictest tier yes ask escalate to the human permission dialog Antigravity: force_ask — always prompts, ignores "Always Allow" the model cannot self-approve no human tier 4 · confirmation required · confirm !== true? the model must state the intended effect inside the call yes deny re-invoke with confirm: true preview with dryRun: true first — fixed in the same turn confirm: true · or exempt 5 · idempotency key missing where required? idempotency "required" and no idempotency_key in the input yes deny supply idempotency_key reusing a key is safe; a new key is a new operation key present · or optional 6 · high-risk or irreversible mutation? the call is clean — steer it, don't block it yes allow proceed — with dry-run steering context injected: a dryRun preview is available read · low-risk · reversible allow reads get zero noise on the hot path every rule mirrors an executor refusal — the generated conformance test enforces hook ↔ executor agreement.
fig 06 · the hook decides in the executor's order — deny, ask, or allow before the model burns a turn

The hook mirrors the runtime — it never replaces it

Section titled “The hook mirrors the runtime — it never replaces it”

Every rule the hook applies is one the runtime executor would apply anyway, in the same order: unknown, unapproved, confirmation, then idempotency. The hook is the outer ring, and it fails open — uninstall it and the runtime still refuses every unsafe call.

So what does the hook buy? Two things:

  • Fewer wasted turns. A missing confirm: true or idempotency_key is denied right in the harness, with the exact fix named, so the model repairs the call on the same turn instead of spending a full round trip to learn what the runtime would have told it.
  • Real human authority. The human-approval tier escalates to a person — a model-supplied argument can never satisfy it.

The outer ring is only honest if it agrees with the inner one. A generated conformance test (tests/conformance.test.ts, the “hook ↔ executor agreement” suite) checks that hookcore.decide() asks or denies exactly where the executor would refuse. If the two ever disagree, the bundle’s own tests fail.

allow and deny map straight across in every harness. The interesting case is ask — the human-approval tier, where a person must sign off and the model must not be able to approve itself past the gate:

Harnessask becomesWhy
Antigravityforce_askAlways prompts the user, ignoring any cached “Always Allow” — the model cannot self-approve past it.
Claude CodeaskEscalates to the real permission dialog; the decision travels in the hook’s JSON output.
Codexdeny (fail-closed)Codex’s PreToolUse has no interactive ask, so the only honest enforcement is to block and route the operation through Codex’s own human approval flow.

One Antigravity detail: its hooks fire for every tool, built-ins included, so the shim passes any tool not in this bundle’s catalog straight through. The deny-unknown guard applies only to this bundle’s own operations.