Three coding agents document a hooks system. One of them works.
All three publish documentation for one. In all three cases the documentation was wrong, and in two of them it was wrong in a way that would have silently produced no evidence at all.
I spent a week building Provene, a tool that writes signed evidence receipts for AI-generated code changes — what an agent changed, what was run against it, and who observed that. To do it I had to integrate with three coding agents’ lifecycle hooks.
That is the interesting result, so here it is, with the commands.
Versions, all from runs on one machine: Antigravity 2.11.0.0, Gemini CLI 0.57.0, Codex CLI 0.147.0-alpha.6.5. Every claim below is dated and falsifiable against those.
Antigravity: documents hooks that never fire
Google’s Antigravity ships a documented hooks.json with PreToolUse, PostToolUse and Stop. I wrote a probe — a script that appends whatever arrives on stdin to a log and exits silently — registered it on all three events in both plausible schema shapes, and ran ordinary sessions.
The log was never created. Not empty: absent. Across multiple sessions, including fresh conversations rather than resumed ones, with a global search of the filesystem afterwards to rule out it landing somewhere unexpected.
Four more experiments, all negative. There is no CLI — agy, antigravity and antigravity-cli are all absent from PATH, and the installation directory contains five executables, none of them a command-line entry point. mcp_config.json is zero bytes. The plugins are content packs, not lifecycle hooks.
There is an open thread on Google’s own forum reporting the same thing on 1.107.0, where the response was “try 2.5.5”. I was on a version three releases past that.
What Antigravity does do is edit real files in your repository. So the change is there to attest and nothing announces it. The only remaining source is a transcript the agent writes for its own replay — which contains the developer’s prompts in plaintext, the model’s reasoning, and the full text of every file written. Reading that safely is an allowlist of four fields and nothing else.
Gemini CLI: hooks fire, and the reference omits the field that matters
Gemini CLI’s hooks work. SessionEnd fires even when the CLI aborts with a critical error, which I only know because I watched it happen.
But its published hooks reference does not mention is_background on run_shell_command. The source does: ShellToolParams in packages/core/src/tools/shell.ts declares command, description, dir_path, is_background and delay_ms, and the tool returns early with a PID when the flag is set.
That field is the only thing separating a command nobody waited for from a verification run asserting it passed. An emitter written strictly from the published reference records backgrounded test suites as passing tests.
Read the reference to learn the contract. Read the source before trusting that a field does not exist.
Codex CLI: hooks that are off by default and fail silently when off
OpenAI’s Codex has a real hook system, with a payload almost identical to Claude Code’s. Getting to that took five rounds of experiment, and four of them were spent discovering that the shape everyone had written down was wrong.
Two things conspire:
Hooks are behind a feature flag. [features] hooks = true in ~/.codex/config.toml — formerly codex_hooks, which still parses and emits a deprecation warning naming the new one. With the flag unset, a perfectly valid hooks file produces no error, no log line, and no hooks.
A misplaced key parses clean and registers nothing. The config wants event names as keys, each mapping to groups that carry their own hooks array. Put the command one level up — which is what every secondary source suggests — and it deserializes without complaint into a group with an empty hook list.
Those two together mean “I configured it and nothing happened” has at least three causes that look identical. Two separate rounds of experiment concluded “Codex hooks do not fire” from a configuration that declared none.
What broke the deadlock was a positive control: point a hook at a binary that cannot possibly exist. If the agent reports a hook execution failure, hooks fire and the question becomes why your script didn’t run. If it says nothing, the hook was never reached. Without that, a negative result is unfalsifiable.
Then the genuinely interesting part. Codex has no failure event, and the embedded JSON Schema for its post-tool payload declares no exit_code, status, success or error field. That reads like a dead end for anything wanting to record whether a test passed.
It isn’t. PostToolUse fires when a tool succeeded and does not fire otherwise — verified three ways: a script that printed normally and exited 1 did not fire it, the same script exiting 0 did, and a tool blocked by a PreToolUse hook did not. So the occurrence of the event is the outcome.
Note what that test required. exit 7 does not distinguish “succeeded” from “ran”, because it aborts the shell. A script that runs to completion and returns non-zero does. Had I stopped at exit 7, I would have concluded PostToolUse means “ran” — and shipped an adapter that silently records every failing test suite as a pass.
The part where I was wrong too
None of this is a story about vendors being careless and me being careful.
I shipped four consecutive releases of my own tool with a path-comparison bug that dropped all command evidence on macOS. os.tmpdir() reports /var/folders/…; git rev-parse --show-toplevel reports /private/var/folders/…. Same directory, two spellings, and my comparison was lexical — so on any checkout under a symlinked path, every command lost its outcome and the receipt carried no verification evidence at all. Silently, in the direction that looks safe.
My tests passed the whole time. They ran on Linux, where /tmp is /tmp. The CI matrix went red at the offending commit and stayed red for four releases while I kept reporting green from my own machine.
I found it because CI told me, eventually, and I finally looked.
What I actually take from this
Provene’s entire proposition is that a receipt asserts only what was observed, and that the absence of a claim is never evidence of its opposite. It turns out that is also the only workable methodology for integrating with these agents:
- A silent parse means nothing. Configuration that loads without error is not configuration that works. Build a positive control before you trust a negative result.
- Absence of an event is not absence of a fact. It can mean “did not happen”, “not wired up”, “we weren’t listening”, or “the format changed”. Collapsing those into one is how a tool reports zero and means unmeasured.
- The source is the contract; the documentation is a hint. Three for three.
So the emitter spec now separates, per agent, what was verified by running it from what was only read. Gemini’s AfterTool payload is still in the second column — hooks fire and the session-end path is confirmed, but an API quota stopped me before any tool executed, and I would rather say that than imply otherwise.
If you use Cursor, or any agent I haven’t touched: the experiments are worth more than the adapter. Which lifecycle events exist, whether they fire, what the payload names its fields, and whether a command’s outcome is recoverable without parsing output. None of it is answerable by reading. All of it is an afternoon with the tool installed.