Skip to content

Install the hooks for Claude Code and Gemini CLI too - #9

Open
cat0825 wants to merge 1 commit into
llm-learner:mainfrom
cat0825:multi-agent-hooks
Open

cat0825 wants to merge 1 commit into
llm-learner:mainfrom
cat0825:multi-agent-hooks

Conversation

@cat0825

@cat0825 cat0825 commented Sep 25, 2026 •

Copy link
Copy Markdown
Contributor

Follow-up to #8, which pooled one budget across every local agent.

The asymmetry this fixes

#8 made the asymmetry visible: a Codex turn was scored 0-10 by the local scorer, while a Claude Code or Gemini CLI turn was counted at a flat two points — because only Codex had a hook installed. One hook script and one scorer already served every agent; only the installer was Codex-only.

So "the same scoring standard for every agent" is one installer away. This PR is that installer.

What it does

A single Agent registry describes each product's config file, event names, and schema quirks, and build_hooks() writes each one's own shape:

Agent Config file Events
Codex $CODEX_HOME/hooks.json SessionStart, UserPromptSubmit
Claude Code $CLAUDE_CONFIG_DIR/settings.json SessionStart, UserPromptSubmit
Gemini CLI $GEMINI_DIR/settings.json SessionStart, BeforeAgent

Codex's mapping is byte-for-byte what hooks.json.template and install_windows.ps1 write, including the commandWindows pair, so #2's and #7's installer contract is unchanged. The new agents need their own handling:

  • Claude Code has no async flag, so its entries are plain. The hook's own 25s scorer timeout keeps it inside UserPromptSubmit's 30s default, and its UserPromptSubmit takes no matcher.
  • Gemini CLI counts timeout in milliseconds (3000/30000), names hooks instead of matching them, and has no UserPromptSubmit or statusMessage field. Getting the unit wrong is silent — the hook is simply killed and stops charging.
  • Codex's payload carries turn_id, which the hook already maps to Codex, so only the other two get HEADROOM_AGENT in the command line. Gemini's id is antigravity, the name the adapter registry and the ledger already use for the ~/.gemini home the two Google tools share.
  • Antigravity's own hook system is deliberately skipped. It has no prompt event; its PreInvocation fires before every model call, which would charge one prompt several times. Antigravity still counts toward the cap through the antigravity adapter.

Also fixes main's red CI (second commit)

main has been failing on Python 3.10 and 3.11 since #8 merged — see run 36122097975, which is the merge commit itself. The cause is not the installer:

headroom.py: error: ambiguous option: --agent could match --agent-home, --agents

--agent belongs to the subcommands while --agent-home and --agents belong to the top-level parser. Up to Python 3.11, argparse resolves option abbreviations while pre-scanning every argument, so it read the subcommand's --agent as an ambiguous abbreviation of the two top-level options and exited 2 — before the subcommand ever saw it. The hook shells out with exactly that argv, so on 3.10/3.11 no turn ever charged: the hook logged scorer_process_failed and failed open, which is silent by design. 3.12 and 3.13 changed the pre-scan, which is why only the two older versions failed.

allow_abbrev=False on the top-level parser is the documented fix for a subcommand option that is a prefix of a top-level one. Verified against CPython 3.10.20 and 3.11.14: both exit 2 before the change, both charge after it. The one visible trade-off is that long-option abbreviations (--state for --state-path) stop working; every caller in the repo, the hook included, already passes full flags. A regression test asserts the hook's exact argv parses and that --agents / --agent-home still do.

Safety

--agents takes auto (default: Codex plus every other agent that already has a home directory here, so it never creates ~/.claude or ~/.gemini for a tool you do not use), all, or a comma list.

Every config is parsed before any is written, so an unparseable file in one agent cannot leave another half-installed. Every written file is backed up first, unrelated hook entries are preserved, re-running is idempotent, and --uninstall never deletes a settings file it did not create.

Tests

test_installer.py is new and self-contained: the Codex contract, the two new agents' schemas, --agents selection, merge/uninstall round-trips, and an end-to-end check that drives the real hook script with a documented Claude Code payload and a documented Gemini CLI payload, asserting both land in the same ledger with the same score and that a redelivered hook charges once.

Full suite passes on CPython 3.10, 3.11, 3.12 and 3.13, and all 12 CI jobs are green.

Overlap with #7

#7 also adds skills/headroom/hooks/install_hooks.py and install.sh, as a Codex-only installer. This PR is a superset of that file and of install.sh. If #7 lands first I will rebase and keep #7's test_platform.py split; if this lands first, #7 can drop those two files. Nothing in #7's test_platform.py assertions is broken by this change — the Codex output is unchanged.

Pooling the budget across agents made the asymmetry visible: a Codex turn was
scored 0-10, while a Claude Code or Gemini CLI turn was counted at a flat two
points, because only Codex had a hook installed. One hook script and one
scorer already served every agent; only the installer was Codex-only.

A single Agent registry now describes each product's config file, events, and
schema quirks, and build_hooks() writes each one's own shape:

  codex   $CODEX_HOME/hooks.json              SessionStart, UserPromptSubmit
  claude  $CLAUDE_CONFIG_DIR/settings.json    SessionStart, UserPromptSubmit
  gemini  $GEMINI_DIR/settings.json           SessionStart, BeforeAgent

Codex's mapping is byte-for-byte what hooks.json.template and
install_windows.ps1 write, including the commandWindows pair. The new agents
need their own handling:

- Claude Code has no async flag, so its entries are plain; the hook's own 25s
  scorer timeout keeps it inside UserPromptSubmit's 30s default.
- Gemini CLI counts timeout in milliseconds, names hooks instead of matching
  them, and has no UserPromptSubmit or statusMessage field.
- Codex's payload carries turn_id, which the hook already maps to Codex, so
  only the other two get HEADROOM_AGENT in the command line. Gemini's id is
  "antigravity", the name the adapter registry and the ledger already use for
  the ~/.gemini home the two Google tools share.
- Antigravity's own hook system has no prompt event (PreInvocation fires
  before every model call, which would charge one prompt several times), so
  only Gemini CLI is wired up.

--agents takes auto (default: Codex plus every other agent already installed
here), all, or a comma list. Every config is parsed before any is written, so
an unparseable file in one agent cannot leave another half-installed, and
--uninstall never deletes a settings file it did not create. install.sh is the
POSIX wrapper, and test_installer.py covers the Codex contract plus the two new
agents, including an end-to-end check that one turn from either agent lands in
the same ledger with the same score.
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.

1 participant