The Google Antigravity harness control model, explained
Antigravity is Google’s agent-first development ecosystem — the IDE, the agy CLI that replaced Gemini CLI in May 2026, extensions for VS Code, JetBrains, Zed and Xcode, and an enterprise tier under Gemini Enterprise. As a harness it has the pieces the others have — a permissions engine, a sandbox, a hook system, an escape hatch — but several of them behave differently from every other harness on the comparison page, and the differences decide how you control it. The binary is closed source, but the control surface is well documented and largely verifiable from the outside: what follows is read from the official docs and verified where possible against the shipped agy 1.1.21 binary.
This page is the reference in two halves. The first is Antigravity’s own controls — what ships natively for controlling tool execution, exactly how each mechanism behaves, and where the model ends. The second is Antigravity with ACP: the hook registration, the config file, the decision mapping, and what a blocked call actually looks like on the wire. Just want to install it? The install guide is one command.
The permissions engine: three lists, one shape
Rules live in ~/.gemini/antigravity-cli/settings.json under a permissions object with allow, ask, and deny arrays. Every rule is an action(target) resource:
{
"permissions": {
"allow": ["command(git)", "write_file(src/)", "mcp(linter/*)"],
"deny": ["command(rm -rf)", "command(sudo)", "write_file(.git/)"],
"ask": ["command(*)", "execute_url(aws.amazon.com)"]
}
}
Seven actions cover the surface: read_file, write_file, read_url, execute_url (browser actuation), command (prefix or anchored regex — command(npm run (build|lint|test))), mcp (mcp(server/tool), mcp(server/*), mcp(*)), and unsandboxed (see the sandbox section). Precedence is strict — deny > ask > allow — so a command(*) in ask forces a prompt past any narrower allow. Two implicit rules to know: allowing write_file on a path grants read_file there, and denying read_file denies write_file.
The defaults are sensible: files inside the workspace are auto-allowed, and everything else — commands, MCP tools, URLs, out-of-workspace files — defaults to ask. During a prompt you can widen the target’s scope for the rest of the turn (a file to its parent directory), with the notable exception of terminal commands, which can’t be scope-edited at the prompt.
The rule language in full — all seven actions, how prefix and regex: command matching decide what command(git) covers, the three presets, and two worked rule sets — is on the Antigravity permissions reference; the hook contract on its own, with a minimal hook you can write, is on the hooks reference.
This is a genuinely good rule language. It’s less expressive than the TOML policy engine Gemini CLI shipped (per-mode rules, five priority tiers, root-owned admin policy files — the high-water mark of the July survey), but it covers files, commands, URLs, browser actuation, and MCP tools in one uniform syntax, and MCP coverage at server/tool granularity is better than most harnesses manage.
Headless: the empty chair, answered correctly
In print mode (-p), there are no prompts, and Antigravity resolves the empty chair the right way: an unobtainable approval is soft-denied — the run continues, exits 0, and a stderr notice names the tool and the allow rule you’d need to pre-grant it. A changelog entry mid-2026 fixed headless runs that previously hung or silently auto-approved such calls, which tells you the behavior is deliberate, tested, and recent.
That puts Antigravity in the small club (dsh, Grok Build’s dontAsk, Gemini CLI’s policy engine before it) that answers unanswerable asks with a deny rather than a shrug — and it matters below, because hook-issued asks inherit the same resolution.
The hook system: five events, and the first native ask
Hooks register in hooks.json — workspace-local at .agents/hooks.json (behind folder trust) or user-global at ~/.gemini/config/hooks.json, shared by the CLI, the IDE, and the app. Five events: PreToolUse, PostToolUse, PreInvocation, PostInvocation, Stop. Tool events take a regex matcher on the tool name (run_command|view_file, browser_.*, or * for everything); handlers are shell commands (type: "command" is the only kind) with a 30-second default timeout, camelCase JSON on stdin, JSON decision on stdout.
A minimal registration, in the shape the file actually takes:
{
"hooks": {
"PreToolUse": [
{
"matcher": "run_command",
"hooks": [
{ "type": "command", "command": "node ~/hooks/check-shell.mjs", "timeout": 30 }
]
}
],
"Stop": [
{ "type": "command", "command": "node ~/hooks/receipt.mjs", "timeout": 10 }
]
}
}
Note the asymmetry: tool events are a list of { matcher, hooks[] } groups; Stop is a flat list of hook commands with no matcher.
What arrives on stdin. A PreToolUse payload carries the call as toolCall: { name, args }, plus conversationId, workspacePaths[], modelName, and stepIdx. The native tool names are Antigravity’s own — run_command, view_file, write_to_file, replace_file_content, multi_replace_file_content, grep_search, find_by_name, list_dir, search_web, read_url_content, invoke_subagent — and a shell call’s command string rides in args.CommandLine (verified live; not command). PostToolUse adds an error string, and fires on non-tool steps too, with toolCall null.
What goes back on stdout. A PreToolUse hook answers:
{ "decision": "deny", "reason": "…" }
where decision is one of allow, deny, ask, force_ask, deny_unless_prior_grant — plus an optional permissionOverrides array that can grant action(target) resources for the turn. ask prompts the human but respects cached “Always Allow” grants; force_ask prompts unconditionally, ignoring them. No other mainstream harness lets a hook hand a call to the human as a first-class outcome — Claude Code’s hooks can ask, but Grok Build’s gate is allow/deny only, and most others are deny-only. For an external policy layer, force_ask is the exact primitive you want: an approval requirement that a locally cached grant can’t pre-empt. PreInvocation/PostInvocation can inject steps into the loop, and PostInvocation can force-continue or terminate; Stop hooks can re-enter the loop with decision: "continue" (with a built-in cap on consecutive continuations, so an always-blocking stop hook can’t hold the agent hostage forever — a thoughtful touch).
Four sharp edges, all verified:
- A failing hook blocks the call. Crash, timeout, non-zero exit — the harness reports
pre-tool hook failedand the tool call does not run. This is fail-closed, the opposite of Claude Code’s and Grok Build’s fail-open cores, and it cuts both ways: no silent lapse of a broken policy hook (good), but a hook whose upstream dependency is down can brick every tool call in the session (bad, if the hook doesn’t carry its own posture). Denials must travel as JSON with exit 0 — there is no exit-code deny channel. - The payload doesn’t name its event. Unlike every Claude-lineage contract, the stdin JSON has no
hookEventName— a handler serving multiple events must be registered once per event with the event passed as a command-line argument (that’s why the ACP registration below runs the same file ashook.mjs pre_tool_use,hook.mjs post_tool_use, andhook.mjs stop). - The hook environment is sanitized. Only whitelisted variables reach the subshell, so env-var-based configuration of a hook mostly doesn’t. Configuration has to live in a file the hook reads itself.
PostToolUsecarries no tool output. The payload has thetoolCalland anerrorstring — not what the tool returned. Output-content scanning isn’t possible from the hook payload alone (the full exchange is in the transcript file the payload points at).
The sandbox
enableTerminalSandbox (default false) confines agent-launched commands with native OS mechanisms — nsjail (Linux), sandbox-exec (macOS), AppContainer (Windows). The approval prompt adapts to sandbox state: enabled, you can approve a single run without restrictions; disabled, you can opt a risky command into containment. The unsandboxed(pattern) permission action exempts matching commands from confinement while still running them through the permission lists. Inside the workspace, a Git repo’s .git directory is mounted read-only even for otherwise-writable agents.
Same caveat as most of the field: it’s off by default. Our sandbox positioning applies unchanged — containment bounds the blast radius; it doesn’t record or decide anything.
Audit
Locally: per-conversation transcripts (transcript.jsonl under the app data directory) — a full exchange record, not a decision ledger. The enterprise tier is where audit becomes a product: central logging of prompts, agent responses, and metadata behind a single admin toggle, with IAM inheritance, VPC Service Controls, workspace/browser/MCP access restrictions, and pooled spend caps on a rolling seven-day meter, via eligible Gemini Enterprise plans. Google states enterprise session data isn’t used to train foundation models.
Grade it honestly: this is the most complete admin plane any harness vendor ships today. Its boundary is its ecosystem — the controls attach at the Gemini Enterprise tier and cover Antigravity. A fleet that also runs Claude Code, Codex, or Cursor gets no shared record, no shared policy, and no shared approval queue from it.
The escape hatch
--dangerously-skip-permissions auto-approves every tool permission request (stream output reports always-proceed) — and it is total. Verified live on 1.1.21 with an A/B pair of otherwise-identical headless turns: without the flag, a registered PreToolUse hook fires before the permission layer; with the flag, the hook layer is not invoked at all — no hook execution, no decision, no record. That’s the opposite of Claude Code (deny rules, hooks, and the circuit breaker survive --dangerously-skip-permissions) and Grok Build (deny rules and PreToolUse hooks fire in every mode including always-approve). On Antigravity, the escape hatch doesn’t just skip the prompts — it removes the interception surface. If a policy hook is part of your control story, the flag is the line it cannot cross, and unattended fleets should treat its use as the event to alert on.
Antigravity with ACP: the hook, the config, and a blocked call
Everything above is the harness’s own surface. ACP stands on one piece of it — the PreToolUse hook — and uses the harness’s own vocabulary to answer. This is the complete picture of what’s on your machine after install, what crosses the wire, and what each outcome looks like from inside a session.
What the installer puts where
curl -sf https://agenticcontrolplane.com/install.sh | bash
The installer detects Antigravity by the agy binary or an existing ~/.gemini/antigravity-cli directory, then:
| Path | What lands there |
|---|---|
~/.acp/hooks/antigravity/hook.mjs |
The hook — one Node file, zero dependencies, MIT on GitHub |
~/.acp/hooks/antigravity/acp.json |
The registration fragment below |
~/.gemini/config/hooks.json |
The fragment merged in under its own acp key — existing hooks in the shared file are kept, never overwritten |
~/.acp/credentials |
The workspace key (the browser opens once to provision it; same file every ACP harness reads) |
~/.acp/config.json |
Optional operational overrides, snake_case keys — see below |
~/.acp/lapse.log |
Appended whenever a call proceeded without a policy check, and why |
The registration fragment, verbatim — the same file, invoked three times with the event as its argument, because the payload doesn’t name its own event:
{
"acp": {
"enabled": true,
"PreToolUse": [
{
"matcher": "*",
"hooks": [
{ "type": "command", "command": "node \"$HOME/.acp/hooks/antigravity/hook.mjs\" pre_tool_use", "timeout": 30 }
]
}
],
"PostToolUse": [
{
"matcher": "*",
"hooks": [
{ "type": "command", "command": "node \"$HOME/.acp/hooks/antigravity/hook.mjs\" post_tool_use", "timeout": 30 }
]
}
],
"Stop": [
{ "type": "command", "command": "node \"$HOME/.acp/hooks/antigravity/hook.mjs\" stop", "timeout": 10 }
]
}
}
matcher: "*" means every tool call — shell, file edits, browser actuation, MCP tools, subagents — across the CLI, the IDE, and the app, since they read the same file. Check it registered from inside Antigravity with /hooks.
What crosses the wire
| Antigravity event | ACP call | What the hook answers |
|---|---|---|
PreToolUse |
POST /govern/tool-use |
{"decision":"allow"}, or deny / force_ask with a reason (below) |
PostToolUse |
POST /govern/tool-output |
{} — an audit and completion record; Antigravity’s payload carries the call and error status, not the tool’s output, so this is not a content scan |
Stop |
— | {"decision":"stop"}, plus a session receipt on stderr. Never continue: a receipt must not be able to keep the agent running |
Before the policy check, the hook maps Antigravity’s native tool names onto the canonical vocabulary the policy floors key on — run_command → Bash, view_file → Read, write_to_file → Write, replace_file_content and multi_replace_file_content → Edit, grep_search → Grep, find_by_name and list_dir → Glob, search_web → WebSearch, read_url_content → WebFetch, invoke_subagent → Task — and lifts args.CommandLine into the canonical command field. The native name travels alongside as client_tool_name, so the audit row shows what Antigravity actually called. Unknown and MCP tool names pass through unchanged.
The three outcomes, as Antigravity sees them
| ACP decision | Hook stdout | In the session |
|---|---|---|
| allow | {"decision":"allow"} |
The call runs. Nothing printed. |
| deny | {"decision":"deny","reason":"[ACP] Denied by policy: <reason>"} |
The call does not run; the reason is what Antigravity has to show for it. |
| ask | {"decision":"force_ask","reason":"[ACP] Approval required: <reason>"} |
A native Antigravity approval card, which a cached “Always Allow” cannot pre-empt. Headless, Antigravity soft-denies it itself — run continues, exit 0, stderr notice. |
What a blocked call looks like
Take the case that was verified live on agy 1.1.21 (2026-08-26): the agent issues a recursive root delete. Antigravity hands the hook this on stdin (abridged):
{
"toolCall": { "name": "run_command", "args": { "CommandLine": "rm -rf /" } },
"conversationId": "…",
"workspacePaths": ["/Users/you/project"]
}
The hook posts it as tool_name: "Bash", client_tool_name: "run_command", tool_input: { "CommandLine": "rm -rf /", "command": "rm -rf /" }, and answers on stdout:
{
"decision": "deny",
"reason": "[ACP] Denied by policy: hardline floor: recursive delete of the root filesystem — blocked unconditionally; this pattern cannot be allowed by policy or approval"
}
Exit code 0, the call never runs, and the audit row records the decision, the reason, both tool names, and the session. The reason text is exactly the gateway’s; the [ACP] prefix is the hook’s.
That particular reason comes from the hardline floor: a short list that denies in every workspace, above policy — no tenant setting, tier default, or standing approval can allow it. The list is deliberately narrow, on the bar of “no legitimate agent task ever needs it”: recursive delete of the root filesystem, a system directory, or the home directory (quoting and ${HOME} spellings included); mkfs; dd or a redirect onto a raw block device; a fork bomb; kill -1; shutdown/reboot/halt/poweroff, init 0/6, systemctl poweroff. Commands laundered through sh -c '…' or eval '…' are scanned as commands, not prose. Everything softer is ordinary policy, which your workspace sets.
At the end of the session, the Stop hook prints one line to the scrollback:
[ACP] Session receipt: 14 tool calls governed · 1 denied · 2 held for approval — review this session: https://cloud.agenticcontrolplane.com/sessions/<conversationId>
Configuration: a file, not the environment
Because Antigravity sanitizes the hook’s environment, the hook reads ~/.acp/config.json (env vars win when they do reach it):
| Key | Env equivalent | Meaning |
|---|---|---|
agent_tier |
ACP_AGENT_TIER |
interactive (default) or background; CI=1 in the environment also resolves to background |
check_timeout_ms |
ACP_CHECK_TIMEOUT_MS |
Decision budget, default 4000 — far under the registered 30-second hook timeout, so the hook always answers before Antigravity’s error path |
govern_base / console_base |
ACP_GOVERN_BASE / ACP_CONSOLE_BASE |
Gateway and console origins |
shadow |
ACP_SHADOW |
Set to off to silence shadow-mode advisory notices |
Unattended fleets should pin "agent_tier": "background" here explicitly. The tier changes the failure posture, which is the next thing to know.
Failure posture, on a fail-closed harness
Antigravity’s core would block every call if this hook crashed, timed out, or exited non-zero. So it never does: every outcome — including its own crash handling — travels as JSON with exit 0, and the posture for gateway trouble is the hook’s own:
- Interactive tier: gateway unreachable → the call proceeds, loudly. stderr gets
[ACP] ⚠ UNGOVERNED: gateway unreachable (<detail>) — <tool> proceeded WITHOUT policy check. Lapse logged to ~/.acp/lapse.log.and the lapse log gets a durable line. An ACP outage must never brick an attended session. - Background tier: gateway unreachable → the call is denied. Nobody is watching, so the block is the safety net.
- One retry on transport failure; an HTTP error status is the server answering and is never retried.
- No credential on the machine → every call proceeds with one
[ACP] ⚠ UNGOVERNED: no credentialwarning per invocation and a lapse line; nothing is blocked before you’ve connected.
The limits, stated plainly
--dangerously-skip-permissionsremoves this hook along with every other. Verified by A/B: no invocation, no decision, no record. Use scopedpermissions.allowrules to unblock headless runs instead, and alert on the flag.PostToolUsecan’t see output, so the post-call record is a completion record, not a content scan.--localisn’t wired for Antigravity. ACP’s on-device engine (~/.acp/policy.json,~/.acp/audit.jsonl, no account) ships for Claude Code, Cursor, and Codex; the installer skips Antigravity under--localand says so. This hook needs a workspace.- Hooks see tool calls, not tokens — and on Antigravity nothing else can either. The Gemini endpoint is fixed, with no base-URL override, so Antigravity’s model traffic can’t be routed through the ACP proxy; the cost X-ray page lists it as tool-call control only.
force_askrendering in the interactive TUI is verified offline against the documented contract; the live pass exercised headless soft-deny, the floor deny, and the receipt.
Where the model ends
- No decision ledger below enterprise. Transcripts record what happened; nothing records what was decided and why — which rule matched, what was asked, who approved. The enterprise audit toggle exists precisely because that record has buyers.
- Config is workspace-writable. Global settings and hooks live in the user profile, but workspace
.agents/hooks.jsonloads behind folder trust — trust the folder and its hooks are live. - PostToolUse can’t see output, so no native post-hoc content control.
- The escape hatch is total.
--dangerously-skip-permissionsremoves hooks from the call path entirely (verified) — no other mainstream harness we’ve tested unhooks its own interception layer with one flag. - One vendor’s fleet. The controls — including the enterprise plane — cover Antigravity. The moment your fleet is heterogeneous, so is your control surface.
Fleet running more than Antigravity? One hook puts ACP’s policy, approvals, and audit record on every Antigravity tool call — the same policy set that runs your Claude Code, Codex, Cursor, and Grok Build sessions.
Frequently asked questions
How do permissions work in Google Antigravity?
Three lists in ~/.gemini/antigravity-cli/settings.json — allow, ask, deny — of action(target) rules like command(git), write_file(src/), or mcp(sql/execute_mutation). Deny beats ask beats allow. Workspace files are auto-allowed; commands, MCP tools, URLs, and out-of-workspace files default to ask. In headless mode an unanswerable ask is soft-denied with a stderr notice naming the allow rule you’d need.
Does Antigravity support hooks?
Yes — five lifecycle events (PreToolUse, PostToolUse, PreInvocation, PostInvocation, Stop) registered in hooks.json, with regex matchers on tool names. PreToolUse hooks answer with a decision: allow, deny, ask, force_ask, or deny_unless_prior_grant — making Antigravity the first mainstream harness whose hook vocabulary includes a native ask.
How do I run Antigravity with ACP?
One installer command detects the agy CLI (or an existing ~/.gemini profile), places one zero-dependency Node file at ~/.acp/hooks/antigravity/hook.mjs, and merges its registration into ~/.gemini/config/hooks.json under an acp key — the file the CLI, the IDE, and the app all read, so existing hooks are never overwritten. From the next session every tool call is checked against workspace policy before it runs; ACP deny blocks the call with the reason, ACP ask becomes a native force_ask prompt card, and a session receipt with a review link lands in the scrollback at Stop.
What does a blocked tool call look like in Antigravity with ACP?
The hook answers Antigravity’s PreToolUse with JSON on stdout: {“decision”:”deny”,”reason”:”[ACP] Denied by policy: …”} and the call never runs. For the always-on floor the reason reads, for example, hardline floor: recursive delete of the root filesystem — blocked unconditionally; this pattern cannot be allowed by policy or approval. That exact case — rm -rf / issued through run_command — was verified live on agy 1.1.21.
What happens when an Antigravity hook crashes or times out?
The tool call is blocked. Antigravity treats a failing pre-tool hook — crash, timeout, non-zero exit — as an error and fails closed, the opposite of Claude Code’s and Grok Build’s fail-open cores. A hook you install must therefore always exit 0 and put its answer in JSON, and bring its own posture for its upstream dependencies.
Does Antigravity have a sandbox?
Yes — enableTerminalSandbox (off by default) confines agent-launched commands with nsjail on Linux, sandbox-exec on macOS, and AppContainer on Windows. The approval prompt adapts: with the sandbox on you can lift it for a single run; with it off you can opt a risky command in. An unsandboxed(…) permission rule exempts matching commands from containment while still passing the permission lists.
What is the escape hatch in Antigravity?
–dangerously-skip-permissions auto-approves every tool permission request — and, verified live on 1.1.21, it bypasses the hook layer entirely: PreToolUse hooks are not invoked at all under the flag. Nothing survives it — unlike Claude Code and Grok Build, whose deny rules and hooks still fire in their bypass modes. Treat it as total.
What do Antigravity's enterprise controls add?
Through eligible Gemini Enterprise plans: central audit logging (prompts, responses, metadata) enabled by a single toggle, restrictions on workspace, browser, and MCP server access, IAM policy inheritance, VPC Service Controls, and pooled spending caps on a rolling seven-day meter. These are admin-plane controls for Google’s ecosystem — they don’t extend to agents running outside Antigravity.
Running Antigravity? See and control every tool call it makes — allow/ask/deny before each call, the reason on every row, one installer:
curl -sf https://agenticcontrolplane.com/install.sh | bash
irm https://agenticcontrolplane.com/install.ps1 | iex
Full Antigravity guide → · see your first checked call → · free up to 5 agents