Antigravity CLI’s hooks.json maps a hook NAME to an object whose keys are EVENT NAMES, each holding a list of handlers (docs):
{ "my-hook": { "enabled": true, "Stop": [ { "type": "command", "command": "..." } ] } }The event is a key, never an "event": "Stop" field. Writing it as a field is the trap, because agy is a Go binary that ignores unknown JSON fields: the file parses without error, the startup log cheerfully reports it as loaded, and the hook then never fires. The load counter is no help either, since it counts what it parsed rather than what it will dispatch.
Two more silent failures sit next to it. The global file must be ~/.gemini/config/hooks.json; a hooks.json in the CLI’s own config dir (~/.gemini/antigravity-cli/) is read and counted but never dispatched from. And the project-local .agents/hooks.json works but is gated on per-workspace trust, so it cannot be the declarative home for anything.
Only five events exist: PreToolUse, PostToolUse, PreInvocation, PostInvocation, Stop. No permission, notification, or session-start event, which bounds what an outside observer can learn. And Stop is not settlement: its payload carries fullyIdle alongside terminationReason, because the loop can terminate with work still pending. Treating a bare Stop as “done” reports ready-for-input while the agent is still going, exactly the distinction pi draws between agent_end and agent_settled.
The general lesson is that a config schema whose consumer ignores unknown fields has no failing case: every wrong shape reports success. Nothing short of asserting the hook’s side effect distinguishes “configured” from “configured correctly”, which is the same discipline as checking a command’s effect rather than its exit code. See Agent Waiting State Splits Into Blocked and Idle.