Wiring the same lifecycle hook across Claude Code, Codex, Antigravity and pi produced four adapters and, at first, four wrong ones. Every failure was silent or actively misleading, and each was silent for a different reason.

Antigravity ignores unknown JSON fields, so writing the event as a value rather than a key parses cleanly and never dispatches (Antigravity Hooks Take the Event as a Key). Codex is the opposite and the easiest: deny_unknown_fields makes it reject a malformed file out loud, and codex exec surfaces the error non-interactively. Its trap is subtler, that hooks is a map of event to a list of groups each holding their own handler list, so a flat list parses and quietly does nothing. pi’s trap was not in the config at all: the adapter fired correctly every time, and a downstream staleness sweep kept erasing the result.

The pi case is the instructive one. Its bin is a #!/usr/bin/env node script, so I asserted for hours that ps -o comm= would report node and built a process match around that. It reports pi, because the process sets its own title. The shebang is evidence about how the kernel starts the program, not about what the process calls itself, and I never ran the thing to look.

Underneath all four is one failure mode: a wrapper that exits 0 unconditionally reports success whether or not its side effect happened, so execFile returning no error, or a hook logging “Completed”, establishes nothing. The only signal that ever distinguished configured from configured-correctly was asserting the state actually changed, which is Verify Tool Output Before Claiming Success applied to configuration rather than commands. See Agent Waiting State Splits Into Blocked and Idle.