Summary
stop-review-gate-hook.mjs (the Stop hook registered by this plugin, hooks/hooks.json, timeout: 900) hangs for minutes on nearly every turn end — even when the review gate is disabled (stopReviewGate: false / codex setup --json reports "reviewGateEnabled": false).
Environment
- Windows 11, Claude Code 2.1.214 (npm global install)
- codex plugin version 1.0.6
- Review gate confirmed disabled via
codex-companion.mjs setup --json
Evidence
Scanned Claude Code session transcripts (50 most recent files across 4 different projects) and aggregated Stop-hook durationMs/timedOut from the hook-run attachment records:
| Project |
Stop hook runs |
Avg duration |
Timed out (900s ceiling) |
| Trading (no project-level Stop hook of its own — cleanest signal) |
25 |
~14 min |
22/25 |
| Agentic OS |
72 |
~12 min |
67/72 |
| Demand-Management-Agentic |
76 |
~6.4 min |
30/76 |
| dip-fabric-starter |
5 |
~3 min |
0/5 |
The "Trading" row is the cleanest signal since that project has no Stop hook of its own to confound the measurement — durations there cluster right at the 900000ms timeout, strongly suggesting stop-review-gate-hook.mjs itself is what's hanging.
Root cause (from reading the script)
In scripts/stop-review-gate-hook.mjs:
function readHookInput() {
const raw = fs.readFileSync(0, "utf8").trim(); // <-- blocks until stdin EOF
...
}
function main() {
const input = readHookInput(); // <-- this can hang
...
const config = getConfig(workspaceRoot); // fast local file read
if (!config.stopReviewGate) {
logNote(runningTaskNote);
return; // <-- intended fast path, never reached if readHookInput hangs
}
...
}
readHookInput() calls fs.readFileSync(0, "utf8"), a synchronous read that blocks until the stdin pipe closes (EOF). On Windows, this can hang indefinitely if the parent process (Claude Code) doesn't reliably close the write end of the child's stdin pipe after writing the hook payload. Because this read happens before the config.stopReviewGate check, the hook's fast, disabled-state early-return path is never reached — the process just sits blocked until the external 900s hook timeout kills it.
Suggested fix
Read stdin with a bounded timeout instead of an unbounded blocking read — e.g. spawn a reader in a worker/child context with a hard deadline, or use a short-timeout polling read, and fall back to {}/empty input if nothing arrives within a few seconds. (We applied an equivalent fix locally to two other, unrelated project-level hooks that had the same failure mode, using a Node setTimeout watchdog around the data/end listeners — happy to share the pattern if useful, though the fix belongs upstream here.)
Impact
This adds several minutes of hung-but-harmless background process time to nearly every Claude Code turn, on every project, on affected machines — independent of whether the review gate feature is actually being used. Given the plugin's own stated purpose is partly to avoid burning extra session time/tokens, this silently works against that goal.
Summary
stop-review-gate-hook.mjs(theStophook registered by this plugin,hooks/hooks.json,timeout: 900) hangs for minutes on nearly every turn end — even when the review gate is disabled (stopReviewGate: false/codex setup --jsonreports"reviewGateEnabled": false).Environment
codex-companion.mjs setup --jsonEvidence
Scanned Claude Code session transcripts (50 most recent files across 4 different projects) and aggregated
Stop-hookdurationMs/timedOutfrom the hook-run attachment records:The "Trading" row is the cleanest signal since that project has no
Stophook of its own to confound the measurement — durations there cluster right at the 900000ms timeout, strongly suggestingstop-review-gate-hook.mjsitself is what's hanging.Root cause (from reading the script)
In
scripts/stop-review-gate-hook.mjs:readHookInput()callsfs.readFileSync(0, "utf8"), a synchronous read that blocks until the stdin pipe closes (EOF). On Windows, this can hang indefinitely if the parent process (Claude Code) doesn't reliably close the write end of the child's stdin pipe after writing the hook payload. Because this read happens before theconfig.stopReviewGatecheck, the hook's fast, disabled-state early-return path is never reached — the process just sits blocked until the external 900s hook timeout kills it.Suggested fix
Read stdin with a bounded timeout instead of an unbounded blocking read — e.g. spawn a reader in a worker/child context with a hard deadline, or use a short-timeout polling read, and fall back to
{}/empty input if nothing arrives within a few seconds. (We applied an equivalent fix locally to two other, unrelated project-level hooks that had the same failure mode, using a NodesetTimeoutwatchdog around thedata/endlisteners — happy to share the pattern if useful, though the fix belongs upstream here.)Impact
This adds several minutes of hung-but-harmless background process time to nearly every Claude Code turn, on every project, on affected machines — independent of whether the review gate feature is actually being used. Given the plugin's own stated purpose is partly to avoid burning extra session time/tokens, this silently works against that goal.