Wire the Antigravity hook
What you’ll have at the end: an Antigravity workspace where every call to
this bundle’s tools passes through the generated PreToolUse hook — unsafe
mutations are denied or escalated to the human before they execute, and
.agent/rules/anvil-safety.md shapes the agent’s behavior alongside it.
Every compiled bundle already contains the hook. anvil compile emits
plugin/antigravity/hooks.json, the shim plugin/antigravity/hook.mjs, the
shared decision core plugin/hookcore.mjs, and the prompt-shaping rules file
.agent/rules/anvil-safety.md. Nothing needs to be generated separately —
this recipe is about copying two files into the right place.
1. Compile the bundle
Section titled “1. Compile the bundle”anvil compile openapi.yaml --manifest anvil.yaml --out generated/paymentsUsing the repo’s payments example, and proving the hook’s two key behaviors (deny an unconfirmed gated mutation; pass foreign tools straight through):
# [docs-tested]WORK=$(mktemp -d)node packages/cli/dist/bin-anvil.js compile examples/payments/openapi.yaml \ --manifest examples/payments/anvil.yaml --service payments \ --out "$WORK/payments" --root "$WORK"# A gated mutation without confirm: true is denied by the hook.DECISION=$(echo '{"toolCall":{"name":"payments_create_refund","args":{}}}' \ | node "$WORK/payments/plugin/antigravity/hook.mjs")echo "$DECISION" | grep -q '"decision":"deny"'# A tool that is not part of this bundle passes through untouched.PASSTHRU=$(echo '{"toolCall":{"name":"read_file","args":{"path":"README.md"}}}' \ | node "$WORK/payments/plugin/antigravity/hook.mjs")echo "$PASSTHRU" | grep -q '"decision":"allow"'rm -rf "$WORK"The deny carries the exact remediation, so the model can fix the call in the same turn:
{"decision":"deny","reason":"This operation creates an irreversible financial mutation. — re-invoke with confirm: true if the user intends the effect."}2. Register the MCP server
Section titled “2. Register the MCP server”Register the bundle’s MCP server in Antigravity’s MCP configuration, pointing
at the bundle’s mcp/server.js (it boots with node mcp/server.js — see the
bundle’s docs/README.md). The hook enforces; the server executes.
3. Copy hooks.json into the workspace
Section titled “3. Copy hooks.json into the workspace”Copy (or merge) plugin/antigravity/hooks.json into your workspace’s
.agents/hooks.json — or the global ~/.gemini/config/hooks.json. The
generated config looks like this:
{ "anvil-payments-guard": { "PreToolUse": [ { "matcher": "*", "hooks": [ { "type": "command", "command": "node ./plugin/antigravity/hook.mjs", "timeout": 10 } ] } ] }}Two things are deliberate here:
- The matcher is
*. Antigravity firesPreToolUsefor its own built-in tools too, and MCP tool namespacing varies by build — so the shim, not the matcher, scopes enforcement. The shim looks each tool name up in the bundle’s committedcatalog.json; anything that is not one of this bundle’s operations is passed through withallow, never denied. Firing on every tool is a small cost for reliable enforcement. - The command path is workspace-relative.
node ./plugin/antigravity/hook.mjsassumes the bundle sits at the workspace root. If it lives elsewhere, change the path to the bundle’s actual location (absolute paths are fine).
4. Keep the rules file
Section titled “4. Keep the rules file”anvil compile also emits .agent/rules/anvil-safety.md at the bundle root —
prompt-shaping guidance Antigravity picks up from the workspace’s .agent/rules/
directory. It restates the same contract the hook enforces (“do NOT
self-confirm”, “supply an idempotency key for payments_create_refund”,
“prefer dryRun: true before any mutation”), listing the concrete gated
operations by tool name. Belt and suspenders: the rules steer, the hook
enforces, and the MCP server’s runtime stays authoritative even if neither is
installed.
5. What the hook decides
Section titled “5. What the hook decides”For each tool call, the shim reads toolCall.name / toolCall.args and emits
{ decision: "allow" | "deny" | "force_ask", reason? }:
- Not one of this bundle’s operations →
allow(pass-through). - Unapproved or unknown-in-catalog operation →
deny(a stale or swapped server exposing something the catalog does not is caught here). - Model-confirm mutation without
confirm: true→deny, naming the flag. - Human-approval operation →
force_ask— Antigravity always prompts the user, ignoring any cached “Always Allow”, so the model cannot self-confirm past it. See Require human approval. - Missing required idempotency key →
deny. - Otherwise →
allow.
If it refuses: that is the hook doing its job. A deny names the exact
missing flag (confirm: true, idempotency_key) — re-invoke with it only if
the user actually intends the effect. A force_ask cannot be satisfied by the
model at all; it hands the decision to the human. Do not weaken the matcher or
remove the hook to get past a refusal — fill in the operation’s manifest entry
or get the human’s approval instead.
- The hook is the outer, advisory check and fail-open by design: if it is never installed, the MCP server’s runtime still refuses the same calls. The hook’s value is denying before the model burns a turn, and escalating human-approval operations to a real prompt.
- The hook carries no per-operation data. It reads
catalog.jsonat runtime, so re-approving and regenerating the bundle updates enforcement with no hook edit.