WEB-4656: add Antigravity 2.0 hooks setup (Subscription mode)#104
Open
thatcatfromspace wants to merge 6 commits into
Open
WEB-4656: add Antigravity 2.0 hooks setup (Subscription mode)#104thatcatfromspace wants to merge 6 commits into
thatcatfromspace wants to merge 6 commits into
Conversation
Mirror the claude-code/hooks/ layout for Google Antigravity 2.0 (subscription
mode). Adds a user-level installer, an MDM device-wide installer, four hook
scripts (PreToolUse + 3 telemetry events), and integration tests.
Wire format verified against AgusRdz/chop:
- Settings file: ~/.antigravity/settings.json (canonical across OSes via
os.path.expanduser("~"))
- Stdin payload: snake_case {session_id, cwd, hook_event_name, tool_name,
tool_input}
- Stdout payload: camelCase {hookSpecificOutput: {hookEventName,
permissionDecision, ...}} — only emitted to override the default allow
- Tool-name matchers cover both "bash" and "Bash" casings
- PreToolUse POSTs to ${gateway}/hooks/antigravity with the existing
PretoolRequestBody shape (unbound_app_label: "antigravity")
- Fail-open on every infra error path — never block the agent on our infra
Tested with unittest (matching the existing claude-code/hooks test convention):
24 tests pass — 10 install/merge/clear integration tests + 14 hook-script
subprocess tests using a fake-curl shim and the chop-verified golden payloads.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Flatten the three telemetry hook scripts (post_tool_use, user_prompt_submit, session_start) from a main()/__main__ wrapper to inline body. Each was ~28 lines of identical boilerplate doing import + call helper + exit; trimmed to the load-bearing 14 lines while preserving fail-open behavior. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…llowlist Three CRITICAL fixes from elite-pr-reviewer plus two WARNINGs. CRITICAL #1: MDM install now writes ~/.unbound/config.json per user. The hook scripts read this file at runtime via _common.py::load_credentials. Without it, every PreToolUse silently fail-opens and org policy is never enforced on managed devices. New _write_unbound_config_payload mirrors claude-code/hooks/mdm/setup.py's pattern: privilege-dropped fork, tmp + rename, O_NOFOLLOW + 0o600, dir 0o700. Called BEFORE the settings.json merge so a settings failure never strands a device with an entry-point hook pointing at missing creds. CRITICAL #2: API keys no longer leak via curl argv. Both notify_setup_complete (user + MDM) and fetch_api_key_from_mdm now use stdlib urllib.request so the Authorization / X-API-KEY header stays inside our process. Previously ps auxe / /proc/<pid>/cmdline exposed the secret to every other user on the device. The hook-script POST in scripts/_common.py is left as-is per review notes — it runs under the user's own UID so the leak is bounded by user permissions, not cross-user. CRITICAL #3: PreToolUse / PostToolUse matchers switched from the "Bash|bash|Write|Edit|Read|Glob|Grep|Task" allowlist to the catch-all "*". Antigravity 2.0 ships at minimum with WebFetch, WebSearch, MultiEdit, NotebookEdit, TodoWrite (none of which were in the allowlist) — every unfamiliar tool was silently bypassing the policy gate. Server-side filtering via the gateway's APP_NATIVE_FILE_TOOLS / tools_to_check is the correct defang point, not the matcher. WARNING fixes: - mdm/setup.py settings.json read now uses O_NOFOLLOW, consistent with the script-write loop above it. - scripts/_common.py drops the -L flag from the hook-POST curl. Gateway is one hop; following redirects masks misconfig instead of surfacing it. Tests: - antigravity/hooks/test_setup.py: matcher tests rewritten to assert catch-all "*" (per-event), regression assert no "|" allowlist, plus a new TestNotifySetupCompleteNoCurl class that puts a fake curl shim on PATH and asserts it's never invoked. - antigravity/hooks/mdm/test_setup.py: new file. Covers the credentials write (api_key present, 0o600 mode, 0o700 dir mode, preserves unrelated fields), the full install payload (config + scripts + settings all land), the ordering invariant (config written before settings), the catch-all matcher shape, and curl-shim assertions for both notify_setup_complete and fetch_api_key_from_mdm. Skipped per brief: - WARNING #4 (gateway URL string replace): speculative, "don't design for hypothetical futures." - WARNING #5 (--foo=bar form): matches claude-code convention. - WARNING #8 (operator log on hook failures): out of scope for v1. - INFO #9-12: cosmetic / matches claude-code / out of scope. Decisions made: - Catch-all matcher uses "*", not "" or omitted, to match claude-code's established settings.json convention (verified in tree). Both forms work per Antigravity docs; "*" is what every other tool's setup ships. - fetch_api_key_from_mdm was also migrated to urllib though the brief only explicitly named notify_setup_complete — same exact leak (bearer token on curl argv), same fix, same blast-radius file. - MDM --clear path does NOT remove ~/.unbound/config.json (matches the claude-code MDM clear behaviour; config is shared across tools so we don't own removal). Test commands run (all green): cd antigravity/hooks && python3 -m unittest test_setup.py -v # 13 ok cd antigravity/hooks/scripts && python3 -m unittest test_hooks.py -v # 14 ok cd claude-code/hooks && python3 -m unittest test_setup.py -v # 7 ok cd antigravity/hooks/mdm && python3 -m unittest test_setup.py -v # 10 ok Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
e47ec0f to
0310cc2
Compare
- UserPromptSubmit hook gets async: True so prompt submission doesn't block on the telemetry curl call (matches PostToolUse and SessionStart). Same fix in user-level and MDM setup. - write_unbound_config (user-level) now uses O_NOFOLLOW on the config file open, matching the MDM variant. Defangs symlink redirection of ~/.unbound/config.json. - install_for_user_payload docstring corrected: os.fork uses copy-on-write for arguments, not pickle. Pickle is only used for the return value over the pipe. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
post_to_gateway in antigravity/hooks/scripts/_common.py was shelling out to `curl -H "Authorization: Bearer <api_key>"`, exposing the bearer token on curl's argv. argv is world-readable on Linux via /proc/<pid>/cmdline (default hidepid=0) and on macOS via `ps auxe`, so any local user could harvest the token while a hook was firing. This is the same leak class we already fixed for the setup-time HTTP calls (notify_setup_complete, fetch_api_key_from_mdm); hooks fire on every PreToolUse/PostToolUse/UserPromptSubmit/SessionStart event, so the leak is high-frequency. Fix mirrors the existing setup-time urllib migration: build a urllib.request.Request with headers in the dict (never on argv), POST via a custom opener whose HTTPRedirectHandler.redirect_request returns None so 3xx surfaces as an HTTPError instead of silently following a Location (the no-`-L` rationale is preserved). Fail-open semantics are unchanged: any URLError / HTTPException / OSError / socket.timeout / ValueError / UnicodeDecodeError, any non-2xx status, or any non-JSON body returns None silently. Test fixture replacement: the previous _make_fake_curl PATH shim is obsolete now that the hook never invokes curl. Replaced with a _FakeGateway BaseHTTPRequestHandler bound on 127.0.0.1:<random> that records each request's path/method/headers/body for assertions; tests that previously read `entries[0]["argv"]` now read `gw.requests[0]["headers"]`. New TestNoCurlAtRuntime puts a fake-curl shim on PATH and runs all four hook scripts end-to-end against the local server, asserting the shim was never invoked — locks the migration in so any regression that re-introduces curl will fail. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Contributor
Author
|
Greptile findings addressed:
The P1 security fix migrates
Test counts: |
The previous commits on this branch were built against chop's published
schema, which proved partly wrong against the actual agy CLI. Three
empirical passes (see AGY-EMPIRICAL-FINDINGS.md) locked in the real
contract; this rewrite aligns the integration with it.
What changed and why:
- Install path: ~/.antigravity/settings.json -> ~/.gemini/config/hooks.json.
Verified empirically — that's the single location agy auto-loads.
The old chop-derived path is not read at all.
- Events installed: PreToolUse + PostToolUse only. UserPromptSubmit and
SessionStart are silently dropped by agy 1.0.5's hook parser;
PreInvocation/PostInvocation/Stop register but never spawn the user
process (log "executing command" but no fork+exec). Don't install
hooks we know won't fire — deleted user_prompt_submit.py and
session_start.py outright.
- Hook script install dir: ~/.unbound/antigravity-hooks/ (our own
namespace, sibling to ~/.unbound/config.json) so --clear has one
place to look and agy upgrades can't surprise-delete our scripts.
- Stdin parsing: agy uses camelCase {conversationId, stepIdx, toolCall:
{name, args}, transcriptPath, workspacePaths} with PascalCase arg keys.
Rewrote read_stdin_event + build_request_body to match. The script's
identity (pre_tool_use.py vs post_tool_use.py) determines the hook
event name — agy's stdin payload omits hook_event_name. Added
ANTIGRAVITY_CONVERSATION_ID env fallback for conversationId.
- Tool name mapping: agy is lowercase-only (run_command, view_file,
edit_file, write_to_file, codebase_search, ask_permission). No
bash/Bash duality. Per-tool extractor in _extract_command_and_metadata
maps PascalCase args -> gateway's command + metadata. Unknown tools
fall through to a JSON-stringified args blob so we don't crash on
browser/notebook/subagent tools we haven't taught yet.
- Stdout: bare native-proto {"decision","reason"} — drop the
hookSpecificOutput / permissionDecision wrapper. Verified empirically
with a deny test: agy honors the bare shape and surfaces the reason
verbatim to the model. Empty stdout + exit 0 stays the canonical allow.
- PostToolUse with toolCall: null: agy fires PostToolUse on every step
including non-tool turns. We skip the gateway POST entirely in that
case — no tool identity means no useful telemetry to record.
- Catch-all matcher: "" (empty string), verified to fire on every tool.
Server-side filtering, not the matcher, defangs.
- Gateway event_name: 'tool_use' (matches claude-code/hooks/unbound.py).
The pre/post phase goes in metadata.hook_event_name.
Preserved: --clear sentinel pattern, urllib (no curl shell-out) for both
gateway POST and notify_setup_complete, MDM privilege-drop fork,
non-destructive merge, per-user ~/.unbound/config.json write, --backfill
no-op, --debug plumbing, fail-open contract on every infra error path.
Tests rewritten to match: stdin fixtures use the new camelCase shape;
output assertions are bare native-proto; one test per major agy tool
verifies build_request_body extracts the right command + metadata;
PostToolUse-with-null-toolCall must NOT POST; UserPromptSubmit/
SessionStart test classes deleted; path assertions migrated to
~/.gemini/config/hooks.json. claude-code regression untouched.
Test results: 14/14 antigravity/hooks/test_setup.py,
26/26 antigravity/hooks/scripts/test_hooks.py,
11/11 antigravity/hooks/mdm/test_setup.py,
7/7 claude-code/hooks/test_setup.py (regression, untouched).
See AGY-EMPIRICAL-FINDINGS.md for the verification methodology.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Contributor
Author
sumit-badsara
approved these changes
Jun 4, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds Google Antigravity CLI (
agy1.0.5+) as a supported tool in this repo. Subscription (hooks) mode only. Pairs with theai-gateway-dataPR (#2109) for the control-plane enum.Verified contract (from three empirical passes)
~/.gemini/config/hooks.json(single user-level location){"hooks": {EVENT: [{matcher, hooks:[{type, command}]}]}}""(empty string)PreToolUse,PostToolUseonly.PreInvocation/PostInvocation/Stopparse but never exec.UserPromptSubmit/SessionStartsilently dropped.{conversationId, stepIdx, toolCall: {name, args}, transcriptPath, workspacePaths, artifactDirectoryPath}. PostToolUse addserror, may havetoolCall: nullrun_command,view_file,edit_file,write_to_file,codebase_search,ask_permission(plus browser/notebook/subagent families). Args use PascalCase keys{"decision":"deny","reason":"..."}blocks the tool call; reason reaches the model verbatimWhat's installed
Per user:
~/.gemini/config/hooks.json— registersPreToolUse+PostToolUsehooks pointing at our scripts. Non-destructive merge (third-party hooks preserved).~/.unbound/antigravity-hooks/unbound_pre_tool_use.py— policy decision script. POSTs to${gateway}/hooks/antigravitywithunbound_app_label: 'antigravity', maps response to bare native-proto decision.~/.unbound/antigravity-hooks/unbound_post_tool_use.py— telemetry. Defensively skips POST whentoolCall: null(agy fires PostToolUse on every step including non-tool turns).~/.unbound/antigravity-hooks/_common.py— shared helper for stdin parsing, per-tool command/metadata extraction, urllib-based gateway POST (no curl argv leak), fail-open contract.~/.unbound/config.json— api_key + gateway_url. Atomic write,O_NOFOLLOW,0o600.MDM variant enumerates user homes and runs the per-user installer under privilege drop.
Per-tool field extraction (in
_common.py:_extract_command_and_metadata)command←metadataextrasrun_commandargs.CommandLinecwd: args.Cwdview_fileargs.AbsolutePathfile_path: args.AbsolutePathedit_fileargs.Instructionfile_path: args.TargetFile,code_markdown_languagewrite_to_file""file_path: args.TargetFilecodebase_searchargs.Querytarget_directories: args.TargetDirectoriesask_permissionAction: Targettarget,action,reasonargs: <opaque>Test plan
antigravity/hooks/test_setup.py— 14/14 (install/clear, atomic write, idempotency, third-party preservation, catch-all matcher, unsupported-event regression)antigravity/hooks/scripts/test_hooks.py— 26/26 (agy-shape stdin fixtures × 6 tool names, null-toolCall skip, env-var conversationId fallback, allow/deny/ask, fail-open on every infra error, no-curl regression, gateway URL bake-in)antigravity/hooks/mdm/test_setup.py— 11/11 (MDM creds write, no argv leak, catch-all matcher, unsupported events absent, per-user path migration)claude-code/hooks/test_setup.pyregression — 7/7 (no collateral damage)agyinstall once mergedLinear
WEB-4656 (sub-issue of WEB-4655).
Follow-up tickets, not blocking this PR
APP_NATIVE_FILE_TOOLS['antigravity']should use agy's real tool names from the table above.~/Library/Application Support/Antigravity/User/).Greptile Summary
This PR adds Antigravity CLI (agy 1.0.5+) hook support in subscription mode, wiring PreToolUse (policy gate, synchronous) and PostToolUse (telemetry, async) into ~/.gemini/config/hooks.json. The implementation is grounded in three empirical passes against the real agy binary and replaces an earlier, incorrect scope that targeted unsupported events and a config path agy never reads.
Confidence Score: 5/5
The change is safe to merge: both hook scripts and installers correctly implement fail-open behavior, O_NOFOLLOW credential writes, and urllib-only authenticated calls.
All previous security concerns are addressed in the current code. The two remaining findings are narrow defensive hardening points that do not affect normal operation or policy enforcement.
antigravity/hooks/scripts/_common.py (_NoRedirectHandler) and antigravity/hooks/setup.py (rewrite_gateway_url_in_file) warrant a second look, but neither blocks merge.
Important Files Changed
Sequence Diagram
sequenceDiagram participant agy as agy CLI participant pre as unbound_pre_tool_use.py participant post as unbound_post_tool_use.py participant common as _common.py participant gw as Unbound Gateway agy->>pre: stdin JSON (PreToolUse, camelCase) pre->>common: read_stdin_event() pre->>common: load_credentials() pre->>common: build_request_body(event, "PreToolUse") pre->>common: post_to_gateway(body, api_key, gateway_url) common->>gw: POST /hooks/antigravity (urllib, Authorization header) gw-->>common: "{"decision":"deny","reason":"..."}" common-->>pre: parsed dict pre->>agy: "stdout {"decision":"deny","reason":"..."} + exit 0" Note over pre,agy: Empty stdout + exit 0 = silent allow agy->>post: stdin JSON (PostToolUse, toolCall may be null) post->>common: read_stdin_event() alt toolCall is dict post->>common: fire_and_forget_telemetry(event, "PostToolUse") common->>gw: POST /hooks/antigravity (async, best-effort) else toolCall is null post->>agy: exit 0 (skip — non-tool step) endReviews (5): Last reviewed commit: "WEB-4656: rewrite Antigravity hook contr..." | Re-trigger Greptile