Skip to content

feat: Codex CLI hooks; fix the budget watchdog and plugin-only tool names (0.53.0) - #69

Merged
Shahinyanm merged 6 commits into
masterfrom
fix/subagent-accounting-and-codex-hooks
Sep 24, 2026
Merged

Shahinyanm merged 6 commits into
masterfrom
fix/subagent-accounting-and-codex-hooks

Conversation

@Shahinyanm

Copy link
Copy Markdown
Member

Claude Code moved to 2.1.281 and Codex CLI to 0.156 since the last release. Two
of our guarantees had gone quiet in the meantime, and Codex grew a hook system
that happens to be ours.

Added — Codex CLI runs token-pilot's hooks

Codex reads lifecycle hooks from ~/.codex/hooks.json or
<repo>/.codex/hooks.json, in the same {matcher, hooks: [{type, command}]}
shape, and blocks a call through hookSpecificOutput.permissionDecision or
exit 2 — the shapes our handlers already emit
(docs). So they are reused
unchanged:

npx token-pilot install-hook --client=codex                  # ~/.codex/hooks.json
npx token-pilot install-hook --client=codex --scope=project  # <repo>/.codex/hooks.json
npx token-pilot uninstall-hook --client=codex

Wired: PreToolUse and PostToolUse on Bash, SessionStart,
UserPromptSubmit. Codex has no file-read tool — the model reads through the
shell, which the bash rules already intercept. apply_patch and spawn_agent
stay out: their payloads are not Claude Code's Edit and Agent, and those
handlers would be reasoning about fields that are not there. Codex will not run
a hook definition it has not been shown, so the installer says to run /hooks.

Verified end to end against Codex 0.156, not only in unit tests: cat big.ts
comes back blocked with our message, and all four wired events complete. The
last part needed a fix — SessionStart failed on every start until the reply
stopped carrying watchPaths, a Claude Code extension Codex rejects, so the
installer now tells the hook which client it serves.

Fixed — the response-budget watchdog was measuring a stub

Claude Code 2.1.271 moved a subagent's report into a SubagentHandback tool
call. Measured on a real run in this repo: the handback message was 5129
characters while the last text block beside it was 17 — so the watchdog scored
a full report at 4 tokens and no tp-* agent could ever exceed its declared
budget. The reply now comes from the handback message, with the last text turn
kept as the fallback for older builds.

The same transcript stopped carrying a usable output total: summing
usage.output_tokens across a run that cost 117k tokens returns 199, because
the records carrying usage are the server-side classifier's. Task events take
the larger of that sum and the reply size.

Fixed — hook advice named a tool plugin-only users do not have

Every deny message pointed at mcp__token-pilot__smart_read and friends, which
on a plugin install are named mcp__plugin_token-pilot_token-pilot__*. With MCP
definitions loaded on demand the model cannot even find the real tool from the
name it was handed. 0.52.0 fixed this for agent frontmatter and missed the
messages; they resolve the prefix at runtime now, and bless-agents writes both
spellings.

Fixed — smaller things

  • git log --oneline -1 denied itself inside a compound command: the -<N>
    bound only counted when a space or end-of-string followed it.
  • Hook commands quote ${CLAUDE_PLUGIN_ROOT}; unquoted they break on a plugin
    path containing a space.
  • The README client matrix promised PreToolUse hooks for Cursor, Gemini CLI,
    Cline and Antigravity. We have never written hooks for them. installation.md
    repeated the claim and said "22 MCP tools" where there are 25.
  • Codex detection checked OPENAI_CODEX / CODEX_MODE; neither string exists
    in the 0.156 binary.
  • tp-run can reach WebFetch / WebSearch — the router matches on wording
    alone and would send "read the changelog" to an agent with no way to open a
    page.
  • omitClaudeMd is documented as of 2.1.271; the reference says so.

Verified

18 new tests — every one pinning a defect was red before its fix — and one
removed along with the Codex env branch it covered. Full suite 1484/1484,
tsc --noEmit clean, build clean. The Codex hook file is asserted for what it
contains and for what it leaves out.

…ames (0.53.0)

Codex grew Claude Code's lifecycle-hook protocol, so the handlers port
unchanged: `install-hook --client=codex` writes ~/.codex/hooks.json (or the
project's) wiring the shell rules, session start and prompt submit. Codex has
no file-read tool, and apply_patch/spawn_agent carry payloads our edit gate
and router would misread, so those stay out.

Claude Code 2.1.271 moved a subagent's report into a SubagentHandback tool
call. The response-budget watchdog was measuring the text stub left beside it
— 4 tokens for a 5 KB report — so no tp-* agent could exceed its budget. It
reads the handback message now. Transcript output_tokens also stopped being
usable (199 for a 117k-token run, because the usage-carrying records are the
classifier's), so task events take the larger of that sum and the reply size.

Hook advice named mcp__token-pilot__* everywhere, which does not exist on a
plugin-only install; the prefix is resolved at runtime now, and bless-agents
writes both spellings.

Also: `git log --oneline -1` no longer denies itself inside a compound
command, hook commands quote ${CLAUDE_PLUGIN_ROOT}, the README client matrix
stops promising hooks for clients we never wrote hooks for, dead Codex env
detection removed, and tp-run can reach WebFetch/WebSearch.
The preamble injected into all 25 tp-* agents told them to call
mcp__token-pilot__smart_read. On a plugin install that tool is named
mcp__plugin_token-pilot_token-pilot__smart_read, so the instruction every
agent reads first pointed at something that does not exist there. The text is
baked at build time, so the namespace is dropped entirely — the agent's own
tools list already carries both spellings, and Claude Code resolves a bare
MCP name since 2.1.271.

Also documents uninstall-hook --client=codex in the CLI help.
Verified live: Codex 0.156 fires the installed PreToolUse hook and blocks
`cat big.ts` with our message — which then told it to use Read(path, offset,
limit). Codex has no Read tool; the line now says where that suggestion
applies, and the head/tail advice beside it already works everywhere.
Measured on Codex 0.156: every session reported "SessionStart Failed";
replacing our reply with a minimal one completed, so Codex validates
hookSpecificOutput and rejects the Claude Code extension `watchPaths`. The
Codex installer now passes --client=codex and the hook leaves Claude-only keys
out for it.

Re-verified live afterwards: SessionStart, UserPromptSubmit, PreToolUse and
PostToolUse all complete, and a blocked `cat` still returns our message.
…ote nothing

The recursive-search rule matched its pattern anywhere in the command line, so
a commit message or a heredoc that mentioned the flag was blocked as if it had
been invoked — the same false positive v0.30.4 fixed for git log. It surfaced
by blocking this release's own commit. The invocation must now start the
command or follow a separator.

Denied shell commands also left no telemetry: only the Read hook wrote events,
so shell interceptions were invisible — and on Codex, where the model reads
through the shell, that is nearly every interception. They now write a
bash_denied diagnostic, awaited so the hook process cannot exit first.
@Shahinyanm
Shahinyanm merged commit ed0f4aa into master Sep 24, 2026
3 checks passed
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