Skip to content

feat(antigravity): warn when a foreign PostToolUse hook shadows the adapter - #19

Draft
zeiddata-dev wants to merge 4 commits into
SuperLogicAI:mainfrom
zeiddata-dev:feat/antigravity-shadow-hook-warning
Draft

feat(antigravity): warn when a foreign PostToolUse hook shadows the adapter#19
zeiddata-dev wants to merge 4 commits into
SuperLogicAI:mainfrom
zeiddata-dev:feat/antigravity-shadow-hook-warning

Conversation

@zeiddata-dev

Copy link
Copy Markdown
Contributor

Closes #9.

agy dispatches a single named hook per event instead of merging them, despite its own docs promising a merge. A user who already has a PostToolUse hook in ~/.gemini/config/hooks.json can install the adapter, see the toggle read "on", and never receive a single row. The failure is completely silent from inside the app.

We can't fix agy's dispatch. This stops the user debugging it blind.

Detection

antigravity_hooks_shadowed() -> bool reports when our hook is installed and some key other than logic-loop registers PostToolUse. It's gated on hooks_status_from, so a user who never turned the toggle on never sees anything.

"Ours" is matched two independent ways:

  1. The owned keyagy namespaces registrations by hook name, so our whole registration lives under logic-loop. Same fact hooks_status_from already relies on.
  2. A command fingerprint — a new const HOOK_FLAG: &str = "--antigravity-hook", now also the single source used by command_for so the two can't drift, plus crate::ingest::MARKER. This is the existing is_ours pattern from ingest.rs/codex.rs.

The second signal is what stops the app warning about itself: a Logic Loop block that a user hand-copied under a different key, or that ended up under a key they renamed, is recognized as ours rather than reported as a stranger.

Both entry shapes are probed — grouped ({matcher, hooks:[…]}, what apply_setup writes) and flat ({type, command}). A foreign hook may legitimately use either, and assuming the shape we happen to write would miss half the shadowing cases.

Constraints held

Detection only. No write path changed at all. apply_setup and strip_ours are byte-identical to before, so setup_is_idempotent_and_preserves_foreign_hooks and remove_restores_original still cover exactly what they did. The foreign entry is never rewritten or removed.

Fails open on every axis. Missing file (read_settings already maps NotFound to {}), unreadable, and malformed JSON all return false. So does any shape the check doesn't recognize — non-object root, PostToolUse that isn't an array, a hook value that isn't an object — because every step is as_object/as_array/is_some_and rather than a guess. The command returns bool rather than Result deliberately: there's no error for the UI to render, and nothing in this path can reach a terminal. The JS side also .catch()es to false.

The file is arbitrary third-party content, so it is only ever read — never executed, never interpolated.

Tests

Eight new cases in antigravity.rs's existing tests module. The four the issue asked for:

  • no hooks file ({}, exactly what read_settings yields)
  • our hook only — asserts installed and not shadowed
  • foreign hook present, adapter not installed — asserts it stays quiet
  • both present — status true and shadowed true, the real silent failure

Plus four guarding the edges: a foreign hook on a different event must not cry wolf; the flat entry shape; our own command under another key must not warn; unrecognized shapes stay silent.

UI

The warning reuses the side panel's existing ingest://tailer-failed strip — same classes, same title-tooltip idiom, rendered directly beneath the blind-sessions strip. Same failure class, same place, same look, no second warning style.

AgentStatusBar drives the check rather than App, because it's the only component that knows when the toggle flips: on mount once antigravity_detect and antigravity_hooks_status resolve, and again on each toggle. A check owned by App would go stale for the rest of the session the moment a user turned agy on.

No DB access, no migration, no new dependency.

The README's Antigravity caveat previously told users to go hunting through hooks.json by hand; it now says the app detects this, and still never edits their hook.

Verification

Draft, because the Rust half is uncompiled. No Rust toolchain on the machine I wrote it on, and the app is macOS-only, so:

  • npx tsc --noEmit — clean.
  • npm run check — 12/12.
  • npm run goldennot run; no extraction prompt was touched.
  • cargo clippy --all-targets -- -D warnings && cargo testnot run. The detector, the HOOK_FLAG refactor, the lib.rs registration and all eight tests are written-to-compile, not proven-to-compile.
  • The issue's final acceptance step — a hand-added foreign hook producing a visible warning — needs a running app on a Mac and has not been done.

Draft so CI compiles it first. For the acceptance step: add a PostToolUse hook under a non-logic-loop key in ~/.gemini/config/hooks.json, turn the Antigravity toggle on, confirm the red strip appears with the rail open and a tab active, and confirm it clears both when the foreign hook is removed and when the adapter is toggled off.

…dapter

agy dispatches a single named hook per event instead of merging them, despite
its own docs promising a merge. A user who already has a PostToolUse hook in
~/.gemini/config/hooks.json can therefore install the adapter, see the toggle
read "on", and never receive a single row — the failure is completely silent
from inside the app.

We cannot fix agy's dispatch, so stop the user debugging it blind:
antigravity_hooks_shadowed() reports when our hook is installed *and* some key
other than `logic-loop` registers PostToolUse. "Ours" is matched by the
--antigravity-hook command fingerprint as well as by the owned key, so a
hand-copied or renamed Logic Loop block never warns about itself. Both the
grouped (matcher + hooks) and flat entry shapes are probed, since a foreign
hook may use either.

Detection only — the foreign entry is never rewritten or removed, keeping
install/remove byte-identically reversible. Fails open on every axis: missing,
unreadable and malformed hooks.json all return false, as does any shape the
check does not recognize, and the command returns bool rather than Result so
there is no error for the UI to handle.

The warning reuses the side panel's existing tailer-failed strip rather than
inventing a second warning style. AgentStatusBar drives the check because only
it knows when the toggle flips; a startup-only check would go stale the moment
the user turns agy on.

Closes SuperLogicAI#9
@SuperLogicAI

Copy link
Copy Markdown
Owner

Review findings (CI green, but flagging before merge):

src-tauri/src/main.rs:8 — headless-hook dispatch hardcodes the literal "--antigravity-hook" instead of using the new HOOK_FLAG const this PR introduces specifically as the single source of truth. If HOOK_FLAG (antigravity.rs:25, currently private) ever changes, main.rs's dispatch silently stops matching what command_for writes into hooks.json — agy's hook invocation falls through to app_lib::run() and boots a GUI window instead of running headless, with no error anywhere. Suggest making HOOK_FLAG pub(crate) and referencing it from main.rs.

Minor, non-blocking:

  • AgentStatusBar.tsx:49 — shadow-hook warning only rechecks on mount/toggle-flip, so a user who manually fixes the foreign hook (per the warning's own instructions) never sees it clear without retoggling.
  • AgentStatusBar.tsx:135 — rapid on/off toggle can race the async antigravityHooksShadowed() call, leaving a stale warning shown for an adapter just turned off.
  • antigravity.rs:121handler_is_ours's ingest::MARKER branch is dead code; nothing writes that Claude-specific marker into an antigravity hooks.json command.

@zeiddata-dev

zeiddata-dev commented Sep 6, 2026

Copy link
Copy Markdown
Contributor Author

Two things below. First is the PR comment, second is the prompt you paste into Claude Code on the machine with the checkout to do the work.

Agreed on all four, fixing in a follow-up commit on this branch.

On the HOOK_FLAG visibility: pub(crate) won't reach main.rs. main.rs is the binary crate (app) and antigravity.rs lives in the library crate (app_lib), so it has to be plain pub. Same effect you're after, main.rs will reference app_lib::antigravity::HOOK_FLAG so the argv match and command_for can't drift.

For the shadow check going stale: moving it into a useEffect keyed on antigravityOn, with a recheck on window focus (the user just came back from editing hooks.json per the warning's own text) plus a slow interval for the case where the app never lost focus. A sequence counter drops results from a check that started before the toggle flipped again, which also closes the rapid on/off race.

Dropping the ingest::MARKER branch from handler_is_ours. You're right, nothing ever writes that into an antigravity hooks.json command.

Will flip out of draft once clippy and cargo test are green.

Prompt for Claude Code (run from the Logic-Loop checkout on branch feat/antigravity-shadow-hook-warning):

Address the review on PR #19 (feat/antigravity-shadow-hook-warning). Four changes, no scope creep, no write-path changes to apply_setup or strip_ours.

1. src-tauri/src/antigravity.rs: change `const HOOK_FLAG` to `pub const HOOK_FLAG`. It must be plain pub, not pub(crate), because main.rs is the binary crate and antigravity.rs is in the app_lib library crate. Add a doc comment line explaining that.

2. src-tauri/src/main.rs: replace the literal "--antigravity-hook" in the args.iter().position() match with app_lib::antigravity::HOOK_FLAG.

3. src-tauri/src/antigravity.rs, handler_is_ours inside shadowing_post_tool_use: remove the `|| c.contains(crate::ingest::MARKER)` branch so the match is only `c.contains(HOOK_FLAG)`. Nothing writes ingest::MARKER into an antigravity hooks.json command, so it is dead code. Update the doc comment above HOOK_FLAG if it still implies MARKER is used here.

4. src/components/AgentStatusBar.tsx: delete the refreshShadowed helper and all three call sites (mount .then, toggle on, toggle off). Replace with a single useEffect keyed on antigravityOn that:
   - if antigravityOn is falsy, calls onAntigravityShadowed(false) and returns
   - otherwise defines check(), which calls antigravityHooksShadowed() and forwards the result to onAntigravityShadowed, .catch to false
   - runs check() immediately, again on window "focus", and on a 15 second setInterval
   - uses a useRef sequence counter: increment at the top of the effect, capture the value, and ignore any .then/.catch result whose captured value no longer matches the ref, so a rapid on/off toggle cannot leave a stale warning
   - cleanup removes the focus listener and clears the interval
   Import useRef. The mount path should just do .then(setAntigravityOn). The toggle handlers should only setAntigravityOn(true/false).

Then run, in order, and fix anything that fails:
   npx tsc --noEmit
   npm run check
   cd src-tauri && cargo clippy --all-targets -- -D warnings && cargo test

Confirm all eight existing shadow tests in antigravity.rs still pass unchanged. Do not add new tests. Commit as one commit with message:
fix(antigravity): reference HOOK_FLAG from main.rs, recheck shadow warning on focus, drop dead MARKER branch

Do not push. Show me the diff and the test output.

One thing to watch when Claude runs it: the repo may not have the react-hooks eslint plugin, so if it adds an eslint-disable comment for exhaustive-deps and eslint complains about an unused directive, tell it to remove the comment.

SuperLogicAI and others added 3 commits September 6, 2026 11:49
…rning on focus, drop dead MARKER branch

Addresses review findings on PR SuperLogicAI#19:

- HOOK_FLAG is now pub (not pub(crate) — main.rs is the binary crate, this
  module lives in app_lib, pub(crate) doesn't cross that boundary), and
  main.rs's headless-hook dispatch matches against it instead of a literal
  "--antigravity-hook" string, so the two can no longer drift.
- The shadow-hook warning previously only rechecked on mount and on toggle
  flip, so a user who fixed the foreign hook by hand (exactly what the
  warning tells them to do) never saw it clear. Now rechecks on window
  focus and a 15s interval, with a sequence ref so a rapid on/off toggle
  can't leave a stale result from an in-flight check.
- Dropped handler_is_ours's dead ingest::MARKER branch — nothing writes
  that Claude-specific marker into an antigravity hooks.json command.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E9BsJXxSYH1VdKpzYBZNWf
@SuperLogicAI

Copy link
Copy Markdown
Owner

Thanks for the detailed fix-up prompt — applied it (commit b4d2b3f pushed to this branch): HOOK_FLAG is now plain pub and referenced from main.rs, dropped the dead ingest::MARKER branch, and reworked the shadow check into a useEffect keyed on antigravityOn with focus + 15s-interval rechecks and a sequence ref guarding the toggle race (matches your spec exactly, including dropping the eslint-disable comment since this repo has no react-hooks plugin).

Local gates all clean on this commit: npx tsc --noEmit, npm run check (8/8), cargo clippy --all-targets -- -D warnings, cargo test (42/42, all 8 existing shadow tests unchanged).

Oddly, CI never triggered on either that push or two follow-up empty-commit retriggers (ef88a5a, 8b14fd8) — zero check-suites created on any of them, even though the same push mechanism worked fine on PR #18 minutes earlier. Not a draft-PR thing (worked on #18 while still draft), not a workflow-file condition (no draft gate in ci.yml), PR isn't locked. Could you push something small from your own machine/credentials to this branch (or just re-push the current HEAD) to see if it's specific to pushes coming from someone else's credentials against your fork? Also worth a quick check that Actions are still enabled in your fork's own repo settings.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Warn when a foreign PostToolUse hook silently shadows the Antigravity adapter

2 participants