feat(antigravity): warn when a foreign PostToolUse hook shadows the adapter - #19
Conversation
…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
|
Review findings (CI green, but flagging before merge): src-tauri/src/main.rs:8 — headless-hook dispatch hardcodes the literal Minor, non-blocking:
|
|
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): 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. |
…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
|
Thanks for the detailed fix-up prompt — applied it (commit b4d2b3f pushed to this branch): HOOK_FLAG is now plain Local gates all clean on this commit: Oddly, CI never triggered on either that push or two follow-up empty-commit retriggers ( |
Closes #9.
agydispatches a single named hook per event instead of merging them, despite its own docs promising a merge. A user who already has aPostToolUsehook in~/.gemini/config/hooks.jsoncan 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() -> boolreports when our hook is installed and some key other thanlogic-loopregistersPostToolUse. It's gated onhooks_status_from, so a user who never turned the toggle on never sees anything."Ours" is matched two independent ways:
agynamespaces registrations by hook name, so our whole registration lives underlogic-loop. Same facthooks_status_fromalready relies on.const HOOK_FLAG: &str = "--antigravity-hook", now also the single source used bycommand_forso the two can't drift, pluscrate::ingest::MARKER. This is the existingis_ourspattern fromingest.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:[…]}, whatapply_setupwrites) 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_setupandstrip_oursare byte-identical to before, sosetup_is_idempotent_and_preserves_foreign_hooksandremove_restores_originalstill cover exactly what they did. The foreign entry is never rewritten or removed.Fails open on every axis. Missing file (
read_settingsalready mapsNotFoundto{}), unreadable, and malformed JSON all returnfalse. So does any shape the check doesn't recognize — non-object root,PostToolUsethat isn't an array, a hook value that isn't an object — because every step isas_object/as_array/is_some_andrather than a guess. The command returnsboolrather thanResultdeliberately: there's no error for the UI to render, and nothing in this path can reach a terminal. The JS side also.catch()es tofalse.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 existingtestsmodule. The four the issue asked for:{}, exactly whatread_settingsyields)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-failedstrip — same classes, sametitle-tooltip idiom, rendered directly beneath the blind-sessions strip. Same failure class, same place, same look, no second warning style.AgentStatusBardrives the check rather thanApp, because it's the only component that knows when the toggle flips: on mount onceantigravity_detectandantigravity_hooks_statusresolve, and again on each toggle. A check owned byAppwould go stale for the rest of the session the moment a user turnedagyon.No DB access, no migration, no new dependency.
The README's Antigravity caveat previously told users to go hunting through
hooks.jsonby 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 golden— not run; no extraction prompt was touched.cargo clippy --all-targets -- -D warnings && cargo test— not run. The detector, theHOOK_FLAGrefactor, thelib.rsregistration and all eight tests are written-to-compile, not proven-to-compile.Draft so CI compiles it first. For the acceptance step: add a
PostToolUsehook under a non-logic-loopkey 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.