Stop-review gate: review timeout equals the hook's own 900s timeout, so a slow review ends the turn with no message

Author: escooterclinicCreated Sep 16, 2026Updated Sep 16, 2026

We use the Codex plugin from Claude Code on a number of machines, and an automated audit of our hooks flagged the Stop hook for how long it can hold a turn open. Reading stop-review-gate-hook.mjs, the review's internal timeout and the hook's own declared timeout are the same number, which I think makes the "review timed out" path unreachable. We don't have the gate enabled anywhere, so this is from reading the code, not from hitting it. Setup: plugin v1.0.6 (byte-identical to main today), macOS, Node 26.7.0, ChatGPT auth.

What happens

plugins/codex/hooks/hooks.json gives the Stop hook "timeout": 900, and the hook spawns the review with a timeout of the same 900 seconds:

javascript
// plugins/codex/hooks/hooks.json
"command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/stop-review-gate-hook.mjs\"",
"timeout": 900

// plugins/codex/scripts/stop-review-gate-hook.mjs
const STOP_REVIEW_TIMEOUT_MS = 15 * 60 * 1000;
...
const result = spawnSync(process.execPath, [scriptPath, "task", "--json", prompt], {
  cwd, env: childEnv, encoding: "utf8", timeout: STOP_REVIEW_TIMEOUT_MS
});

if (result.error?.code === "ETIMEDOUT") {
  return {
    ok: false,
    reason:
      "The stop-time Codex review task timed out after 15 minutes. Run /codex:review --wait manually or bypass the gate."
  };
}

The hook starts first and spawns the child a few milliseconds later, so the child's deadline is always the later of the two, and Claude Code kills the hook while the child is still inside its own window. The hook's only write to stdout happens after spawnSync returns, so at that point it has emitted nothing at all. A review that genuinely runs long therefore leaves the user with a turn that appears to hang for fifteen minutes and then ends with no reason given, instead of the "run /codex:review --wait manually or bypass the gate" note the code is trying to deliver. The branch itself is correct — spawnSync does set error.code === "ETIMEDOUT" on Node 26 — it just doesn't look reachable.

What we ruled out

Not our configuration: stopReviewGate is false in every workspace here, so nothing we set touches this path. Not a stale copy: the shipped stop-review-gate-hook.mjs is byte-identical to main, and main's hooks.json still declares 900. I searched open and closed issues for "stop review gate", "hook timeout", "ETIMEDOUT", "900" and "15 minutes"; #248 and #530 sit next to this one but neither is about the two timeouts being equal, so I opened it separately rather than commenting there.

Reproduction

I haven't sat through a real 900s review, and by inspection it's only these two constants. To see it quickly, set STOP_REVIEW_TIMEOUT_MS and the hooks.json timeout to the same small value, enable the gate, and make the review hang.

One guess, and I may be reading this wrong: if the hook is meant to report its own timeout, the inner deadline needs headroom under the outer one. Which number should move is your call, so I haven't sent a PR picking one. Happy to test a patch.