openai / openai/codex-plugin-cc

Stop review gate blocks Claude on infrastructure errors, causing rewake loops

Open
#248 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
JavaScript
Stars
33.3k
Forks
2.3k
PR merge metrics
No merged PRs in 30d

Description

Summary

scripts/stop-review-gate-hook.mjs conflates two distinct outcomes into the same { decision: "block" } hook
output:

  1. Genuine BLOCK — Codex task returned BLOCK: <reason> (a real policy decision)
  2. Infrastructure failure — the Codex task could not produce a valid ALLOW/BLOCK decision at all (timeout,
    non-zero exit, empty output, invalid JSON)

Combined with Claude Code's rewake-on-block semantic, any transient upstream failure (ChatGPT usage limit, auth
expiry, network blip, Codex app-server crash) creates a sustained rewake loop. Claude replies → hook fires → Codex
still failing → block → Claude rewakes → … The loop burns Claude tokens continuously and produces zero useful review,
and the user has no way out other than /codex:setup --disable-review-gate.

Reproduction

  1. Enable the gate: /codex:setup --enable-review-gate
  2. Induce any transient Codex-side failure. Easiest repro: exhaust your ChatGPT / Codex usage quota.
  3. Finish any Claude turn (even a trivial one).
  4. The hook's spawned codex task exits with status: 1 and empty output. The hook emits { decision: "block" }.
  5. Claude Code rewakes the model; Claude replies; the hook fires again with the same failure; loop continues until
    the user manually disables the gate or the quota resets.

In a real session I observed ~10 consecutive failed jobs in
~/.claude/plugins/data/codex-openai-codex/state/<workspace>-<hash>/state.json, all with:

"summary": "You've hit your usage limit. To get more access now, send a request to your admin or try again at 7:32
PM."
"status": "failed"

Each one produced a BLOCK decision to Claude Code, despite no review having actually run.

Root cause

In scripts/stop-review-gate-hook.mjs:

  • parseStopReviewOutput() returns { ok: false, reason } for both BLOCK: ... responses and any
    unexpected/empty output.

  • runStopReview() returns { ok: false, reason } for timeouts, non-zero exits, and JSON parse failures.

  • main() treats every non-ok result identically:

    if (!review.ok) {
      emitDecision({ decision: "block", reason: ... });
      return;
    }
    

There is no code path for "the review didn't run — don't block, just warn". All infrastructure failures are mapped to
the same blocking decision as a real Codex BLOCK.

Proposed fix

Distinguish outcomes with a tagged return type:

  • { kind: "allow" } — Codex said ALLOW
  • { kind: "block", reason } — Codex said BLOCK
  • { kind: "infra_error", reason } — timeout / non-zero exit / empty / invalid JSON

Then in main():

  • block → existing behavior (emitDecision({ decision: "block", reason }))
  • infra_errorlogNote() with actionable reason, exit without blocking Claude
  • allow → existing behavior

This preserves all correct behavior (real BLOCKs still block; ALLOWs still allow) and only changes the path where the
review cannot run. A user whose Codex is misconfigured or quota-limited now sees a single warning note per turn
instead of an infinite loop.

Happy to submit a PR with this fix plus a regression test (mock spawnSync returning status: 1, assert
emitDecision is not called). Flagging as issue first to confirm the direction before submitting code.

Alternatives considered

  • Circuit breaker (auto-disable gate after N consecutive blocks): mitigates the symptom but doesn't fix the
    conflation. A real sequence of legitimate BLOCKs (user genuinely refusing to fix an issue) would also trigger it.
  • Pattern-match known error strings in main(): fragile, can't keep up with new failure modes.
  • Reuse buildSetupNote() for runtime failures: that path only covers "Codex not installed / not logged in"
    before any task runs, not failures during task execution.
  • Retry on infra error: multiplies token cost when backend is genuinely unavailable.

Environment

  • Plugin version: codex@openai-codex v1.0.4
  • Platform: macOS (Darwin 25.4.0)
  • Node: v22.19.0
  • Auth: ChatGPT (consumer)

Related

No existing open or closed issue covers this specific conflation (searched open + closed for review gate, stop hook, BLOCK, loop, quota, rawOutput, status 1). Adjacent issues: #191 (Windows-specific stdin blocking,
opposite scenario), #213 (user-level default for stopReviewGate, feature request), #152 (/codex:usage command,
quota visibility). None addresses the decision-mapping path in the hook itself.

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with scripts/stop-review-gate-hook.mjs, reading parseStopReviewOutput(), runStopReview(), and main() to trace how BLOCK results differ from timeouts and failed commands. Run the hook tests, then add the proposed spawnSync regression case and verify that genuine BLOCK and ALLOW decisions remain unchanged while infrastructure failures do not call emitDecision.

Written by the indexing model from the issue text.

Assessment

Tech stack
javascript
Domain
devtools
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Quiet
Clarity
Clearly specified
Newbie friendliness
68/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.