Skip to content

feat(agy): Google Antigravity CLI hook integration - #2093

Closed
thelok1s wants to merge 17 commits into
rtk-ai:developfrom
thelok1s:feat/issue-1975-agy-hook
Closed

thelok1s wants to merge 17 commits into
rtk-ai:developfrom
thelok1s:feat/issue-1975-agy-hook

Conversation

@thelok1s

@thelok1s thelok1s commented May 26, 2026 •

Copy link
Copy Markdown

Summary

  • Adds rtk hook antigravity — a PreToolUse hook for the Google Antigravity CLI (agy) that rewrites bare shell commands to their rtk-prefixed equivalents before the model executes them
  • rtk init --agent agy configures ~/.gemini/antigravity-cli/settings.json with the hook and grants command(rtk) permission so retries are auto-approved
  • Handles both simple (tool_name/tool_input) and rich (toolCall.name/toolCall.args) agy payload formats
  • RTK_HOOK_DEBUG=1 logs full request/response pairs to ~/.local/share/rtk/agy-hook-debug.log

How it works

agy's overwrite field in PreToolHookResult is silently ignored (confirmed via debug logging — correct payload sent, original command executed). The hook therefore uses the denyReason strategy:

  1. Hook intercepts run_command / Bash tool calls
  2. If the command has an RTK equivalent, hook returns {"denyReason": "RTK: use 'rtk git log' instead of 'git log' (60-90% token savings)"}
  3. Model reads the reason and retries with the rtk-prefixed command
  4. command(rtk) in the allow list auto-approves the retry — no user prompt

Commands already prefixed with rtk or in the passthrough list pass through with {"allowTool": true}.

Test plan

  • cargo test --all — 1924 tests pass, zero clippy warnings
  • rtk init --agent agy writes correct ~/.gemini/antigravity-cli/settings.json
  • Hook rejects bare git log with denyReason naming rtk git log
  • Hook passes through rtk git log with allowTool: true
  • Hook passes through non-shell tools (e.g. read_file) with allowTool: true
  • RTK_HOOK_DEBUG=1 writes input/output to debug log

Known limitation

agy does not honour the overwrite field in PreToolHookResult — only allowTool and denyReason are implemented. The deny-and-retry pattern produces a ⚠ Tool call denied notice in the agy UI before the model retries with the RTK command. Transparent rewriting will be possible once agy implements overwrite support.

This issue reported in this topic on discuss.ai.google.dev. More visibility on this bug report required.

image Tested in Antigravity CLI v1.0.2

thelok1s and others added 16 commits May 25, 2026 18:30
…-ai#1975)

Add programmatic PreToolUse hook support for the agy CLI (successor to Gemini
CLI) while keeping the existing Antigravity IDE rules-file integration intact.

Changes:
- hook_cmd.rs: process_antigravity_payload + run_antigravity() handle both
  run_command (CommandLine field) and Bash (command field) tool payloads;
  extra args (e.g. Cwd) are preserved in the rewritten response
- init.rs: run_agy_cli_mode(), patch_agy_hooks_json(), agy_hook_already_present(),
  insert_agy_hook_entry(), uninstall_agy_cli() — install/uninstall hooks.json
  at ~/.agents/hooks.json (global) or .agents/hooks.json (project-local)
- main.rs: AgentTarget::Agy, HookCommands::Antigravity (alias: agy), routing
  for rtk init --agent agy and rtk hook antigravity; uninstall dispatch updated
- constants.rs: ANTIGRAVITY_DIR, ANTIGRAVITY_HOOK_COMMAND, ANTIGRAVITY_TOOL_MATCHER
- hooks/antigravity/README.md: document agy CLI vs Antigravity IDE vs Gemini (legacy)

JSON protocol:
  Input:  {"toolCall": {"name": "run_command", "args": {"CommandLine": "git status"}}}
  Output: {"decision": "allow", "toolCall": {"args": {"CommandLine": "rtk git status"}}}
  No output for non-shell tools or commands with no RTK rewrite (pass-through).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… compat)

floor_char_boundary is stable since Rust 1.87 but the Homebrew toolchain does
not expose it. Inline the equivalent scan using is_char_boundary which has been
stable since Rust 1.0.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…rmat

- Write hook config to ~/.gemini/antigravity-cli/settings.json (not ~/.agents/hooks.json)
- Use BeforeTool/matcher format with per-tool entries for run_command and Bash
- Handle both agy payload formats: simple (tool_name/tool_input) and rich (toolCall.args)
- Simple payloads respond with hookSpecificOutput.tool_input; rich preserve all extra args
- Add AGY_CLI_SUBDIR constant; suppress dead_code on ANTIGRAVITY_DIR

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
agy runs hooks outside the user's shell, so bare `rtk` is not on PATH.
Resolve current_exe() at init time and write the full binary path
(e.g. /home/user/.cargo/bin/rtk hook antigravity) so the hook is
always found. Re-init strips stale relative-path entries before
inserting the new absolute-path command.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
agy (Antigravity CLI) fires the run_shell_command BeforeTool hook from
~/.gemini/settings.json but sends a different payload than Gemini CLI:
command is at toolCall.args.CommandLine, not tool_input.command.
The handler was returning pass-through for every agy call.

Detect the rich format first (toolCall.args.CommandLine present), rewrite
the command, and return toolCall.args with CommandLine updated while
preserving Cwd, WaitMsBeforeAsync, and any other fields.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ite format

Fixes the agy hook integration by addressing two root causes found via
binary analysis of the agy binary:

1. Config target: agy reads hooks from ~/.gemini/config/hooks.json (named
   hook format), not from ~/.gemini/antigravity-cli/settings.json (settings
   format). Rewrote init.rs to write the correct named-hook structure:
     { "rtk-antigravity": { "enabled": true, "PreToolUse": [...] } }

2. Response format: PreToolHookResult expects "overwrite" (HookToolCall) not
   "toolCall" to modify a tool call. Confirmed via agy binary protobuf symbols.
   Updated hook_cmd.rs to output:
     {"overwrite": {"name": "run_command", "args": {"CommandLine": "rtk ..."}}}

Hooks are now confirmed firing and rewriting commands (verified via agy
cli.log showing hook execution and the prior error message identifying the
field name mismatch).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ials

agy interprets empty hook stdout as "deny". The hook was previously
outputting nothing for non-shell tools (Read, ListPermissions, etc.),
causing those tool calls to be blocked.

Fix: always write at least "{}" (allow, no modification) for any tool
call that doesn't need a command rewrite. Confirmed by live testing where
Read and ListPermissions were denied by our hook before this change.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
agy's PreToolHookResult.allow_tool (proto field 1, bool) defaults to
false — an empty {} is an implicit deny, not a pass-through. Every
response must include {"allowTool":true} to permit tool execution.

Also adds allowTool:true alongside overwrite responses so rewritten
shell commands are not blocked by the missing allow flag.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ough

agy's PreToolHookResult semantics:
- {"overwrite": {...}} → replace the tool call (implicitly allowed)
- {"allowTool": true}  → allow the original call unchanged
- {"allowTool": true, "overwrite": {...}} → allowTool wins, overwrite ignored

Previously: {} for passthrough → implicit deny (allow_tool defaults to false)
Previously: {allowTool+overwrite} → overwrite silently dropped

Now: overwrite-only for rewrites, allowTool-only for passthrough/skip/ignore.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When the hook rewrites git status → rtk git status, agy's permission
system checks whether rtk is in the allowed-commands list. Without the
grant, the rewritten command is denied even though the original command
would have been approved by the user.

rtk init -g --agent agy now:
- Writes the PreToolUse hook entry into ~/.gemini/config/hooks.json
- Adds command(rtk) to permissions.allow in ~/.gemini/antigravity-cli/settings.json

rtk init -g --uninstall --agent agy reverts both.

Also adds RTK_HOOK_DEBUG=1 support in run_antigravity() for future
protocol debugging (dumps raw stdin to ~/.local/share/rtk/agy-hook-debug.log).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The PreToolHookResult overwrite mechanism is broken under agy's
toolPermission=request-review setting. When the hook returns an overwrite,
the permission review fires on the original binary (git, cargo, etc.) rather
than the overwritten one (rtk), so the model never sees the rewritten command.
Evidence: turnGrants=[command(git)] on user approval of overwritten command.

Switch to deny-with-reason: deny the original command with a denyReason that
names the exact rtk-prefixed replacement. The model reads the reason and
retries with e.g. 'rtk git status', which hits the Allow list (command(rtk))
directly and bypasses the request-review gate entirely.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Remove duplicated test_claude_md_mode_refuses_malformed_block introduced
during conflict resolution across cherry-picks. Apply cargo fmt output.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
denyReason always shows ⚠ warning in every permission mode.
Under toolPermission=always-proceed there is no review gate, so overwrite
works transparently: no warning, no extra LLM call, command silently
rewritten to rtk-prefixed equivalent.

Rich format preserves extra args (Cwd, WaitMsBeforeAsync, etc.) from the
original toolCall.args so metadata is not lost on rewrite.

Under request-review, overwrite is bypassed by the review gate (original
command runs without RTK), but the user is not blocked.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
overwrite alone still hits allowTool=false (proto default), which agy
treats as a denial and shows ⚠. Must set allowTool:true explicitly for
agy to execute the rewritten command without showing a warning.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Debug logging confirmed: hook sends correct overwrite payload but agy
executes the original command unchanged. Only allowTool and denyReason
are honored by agy's hook system.

denyReason: model reads the replacement command and retries with rtk prefix;
command(rtk) in the allow list auto-approves the retry in always-proceed mode.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings May 26, 2026 00:07

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

Adds first-class support for Google Antigravity CLI (agy) by introducing a dedicated hook command and installer, while also hardening UTF-8 boundary handling in a couple of string-slicing utilities.

Changes:

  • Add a new Agy agent target and rtk hook antigravity hook command path.
  • Implement agy CLI install/uninstall logic that patches ~/.gemini/config/hooks.json and updates agy permissions in settings.json.
  • Replace floor_char_boundary usages with manual UTF-8 boundary adjustment loops to avoid panics.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
src/main.rs Adds Agy agent + hook subcommand wiring and uninstall dispatch updates.
src/hooks/init.rs Implements agy CLI init/uninstall, hooks.json patching, and permission allow-list updates.
src/hooks/hook_cmd.rs Adds run_antigravity hook handler and extends Gemini hook to detect a “rich” payload shape.
src/hooks/constants.rs Introduces new constants for agy CLI subdir/layout.
src/cmds/system/pipe_cmd.rs Avoids UTF-8 slicing panic by adjusting to char boundaries manually.
src/cmds/system/json_cmd.rs Avoids UTF-8 slicing panic when truncating long strings.
hooks/antigravity/README.md Updates documentation to describe Antigravity IDE vs agy CLI integration.

Comment thread hooks/antigravity/README.md Outdated
rtk init -g --agent agy
```

**Install (project-local):**
Comment thread src/hooks/init.rs Outdated
let mut settings: serde_json::Value = if path.exists() {
let content = fs::read_to_string(path)
.with_context(|| format!("Failed to read {}", path.display()))?;
serde_json::from_str(&content).unwrap_or(serde_json::json!({}))
Comment thread src/hooks/init.rs Outdated
let mut hooks: serde_json::Value = if path.exists() {
let content = fs::read_to_string(path)
.with_context(|| format!("Failed to read {}", path.display()))?;
serde_json::from_str(&content).unwrap_or(serde_json::json!({}))
Comment thread src/hooks/init.rs Outdated
"enabled": true,
"PreToolUse": [
{
"toolNameMatcher": "^(run_command|Bash)$",
Comment thread src/hooks/init.rs Outdated
Comment on lines +1771 to +1776
// Use the absolute path so agy can find the binary regardless of its PATH.
let rtk_bin = std::env::current_exe()
.context("Failed to resolve rtk binary path")?
.to_string_lossy()
.to_string();
let hook_command = format!("{} hook antigravity", rtk_bin);
Comment thread src/hooks/hook_cmd.rs
Comment on lines +765 to +770
let dir = dirs::home_dir()?.join(".local").join("share").join("rtk");
let mut f = std::fs::OpenOptions::new()
.append(true)
.open(dir.join("agy-hook-debug.log"))
.ok()?;
let _ = writeln!(f, ">>> {}\n", response);
Comment thread src/hooks/init.rs
Comment on lines +1764 to +1765
pub fn run_agy_cli_mode(global: bool, ctx: InitContext) -> Result<()> {
let InitContext { dry_run, verbose } = ctx;
- Fix debug log OpenOptions missing create(true); response log silently
  failed if file did not exist after the earlier input-logging block
- Error instead of silent unwrap_or({}) when settings.json or hooks.json
  contains invalid JSON; corrupt config no longer silently overwritten
- Quote rtk binary path in hook_command so spaces in install paths work
- Expand toolNameMatcher to ^(run_command|Bash|bash)$ to match parser
- Add 8 unit tests: file-missing, no-op/idempotent, invalid-JSON error,
  and lowercase-bash matcher coverage for agy install/uninstall paths
- Update hooks/antigravity/README.md to document global-only install,
  actual file paths, and denyReason protocol (removes stale .agents/ refs)
- Add agy CLI row to main README Quick Start and Supported AI Tools table

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@thelok1s thelok1s changed the title feat(agy): Google Antigravity CLI hook integration (#1975) feat(agy): Google Antigravity CLI hook integration May 26, 2026
@thelok1s

Copy link
Copy Markdown
Author

Resolves #1975

@webermaster

Copy link
Copy Markdown

This would be very useful. any idea on when it might be resolved?

@KuSh

KuSh commented Sep 19, 2026

Copy link
Copy Markdown
Collaborator

Closing in favour of #3711, which adds the same native Antigravity PreToolUse hook on the canonical antigravity target. Both of the current Antigravity pull requests rewrite the command through the overwrite field, with a live capture from a recent agy build showing it is honoured, so the deny-with-reason strategy here is no longer needed. If you have newer evidence that overwrite is still ignored, or you meant the separate agy target to stay its own integration, please say so on #3711 or reply here.

@KuSh KuSh closed this Sep 19, 2026
@KuSh KuSh added the duplicate This issue or pull request already exists label Sep 19, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

duplicate This issue or pull request already exists

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants