Skip to content

Reverse-direction Claude gate (codex-claude-admit + claude PATH shim)

Why

The governor's PreToolUse hook gates Claude's own Agent/Workflow spawns. It does not see the reverse direction: an external harness — Codex/ChatGPT, fleet-dispatch, an llm-council seat — invoking the native claude CLI, which spends the same 5h Claude budget. On 2026-07-21 a Codex loop ran claude -p --model claude-fable-5 repeatedly and burned the entire window before any burn-scan recomputed the tier. Seven ungated call sites were found, all resolving claude through PATH.

Design

Two defenses in the codex-claude-admit verb (CodexClaudePolicy + CodexClaudeLedger):

  1. Tier gate — deny on native RED/BLACKOUT (budget already spent). No governor state → fail closed.
  2. Runaway call-cap — codexClaudeMaxCallsPer5h (12) / codexClaudeMaxCallsPerDay (40), booked atomically in codex-claude-ledger.jsonl. This bites before a burn-scan flips the tier — the lag the incident exploited. YELLOW admits but advises a cheaper model.

The verb is fronted by a claude PATH shim (shim/claude POSIX + shim/claude.cmd) installed ahead of the real binary. It runs the admit and execs the real claude.exe only on exit 0. It gates only non-interactive claude -p/--print; interactive claude passes straight through, so a human launching Claude Code is untouched.

Metering caveat (Kerr)

The cap counts calls, a proxy for burn. A few very large prompts can concentrate cost into fewer than cap calls. The tier gate (native RED/BLACKOUT) is the token-spend backstop; the cap bounds call-rate. Do not read "under 12 calls" as "safe spend".

Controls

Control Effect
UG_CLAUDE_ADMIT_BYPASS=1 Human break-glass — skip the gate for this call (POSIX: =1; cmd: ==1).
UG_CLAUDE_ADMIT_FAIL_OPEN=1 If the governor exe is missing, allow instead of the default fail-closed.
config.json codexClaudeGateEnabled=false Kill-switch — admit everything (loud on stderr).
codexClaudeMaxCallsPer5h / codexClaudeMaxCallsPerDay Tune the runaway caps.

Install

install.ps1 calls install-claude-shim.ps1, which copies the exe to a stable ~/.claude/usage-governor/bin/UsageGovernor.exe, renders the shims with that path, and prepends the shim dir to the USER PATH. Restart Codex / fleet-dispatch listeners after install so they inherit the new PATH.

bin/ has exactly one writer, sync-governor-bin.ps1, called by both install-claude-shim.ps1 and a master deploy.ps1 (no -NoPull), which then asserts the bin copy reports the just-built commit. Before FLT-269 only the shim installer wrote bin/, so two months of deploys refreshed publish/ while every PC's callers kept running a 2026-07-22 build without grok-exec. deploy.ps1 -NoPull (worktree verification, CI) deliberately leaves bin/ alone.

Absolute-path residual — CLOSED caller-side (FLT-176)

A PATH shim only intercepts callers that resolve claude by name. A caller that invokes the absolute ...\.local\bin\claude.exe bypasses it. All seven known incident paths use bare claude, so they were already covered — but callers should not rely on PATH ordering alone. This is now enforced caller-side at every trusted caller: each admits explicitly via UsageGovernor codex-claude-admit before launching claude, then runs it with UG_CLAUDE_ADMIT_BYPASS=1 on the child so the shim never double-books the already-admitted call. Missing/broken governor fails closed (UG_CLAUDE_ADMIT_FAIL_OPEN=1 break-glass).

  • fleet-dispatch (master 7509af5 + parity ec7b992): ClaudeAdmitGate — one admit at the RealJobExecutor.RunClaudeAsync orchestrator, covering the stream-json path and both legacy-argv fallbacks; also gates the run_agent ClaudeAgentRunner shell adapter (an 8th path found in review).
  • llm-council (master 567a760): ProcessCliRunner gates the claude seat (quote-stripped basename match) before prompt-file staging.
  • codex-harness-mirror (master 55db22c): the 4 skills carry inline PS admit guards; the two council skills soft-drop a denied Claude seat instead of aborting the council.

Each landed behind its own council-code-review gate. Gate-design notes for future callers: fail closed on a Process.Start exception (not only a null return); bound the post-exit pipe drain and the admit wait (5s) so a wedged/child-holding governor cannot hang the caller; strip surrounding quotes before a basename match. Resolve the governor exe: FLEET_USAGE_GOVERNOR env → ~/.claude/usage-governor/bin/UsageGovernor.exe → the publish path.