Skip to content

Stop hook hangs until 900s timeout on Windows even when review gate is disabled (stdin EOF never arrives) #530

Open

Description

@manuelniklos87

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.

Activity

  1. CBEPX commented on Sep 28, 2026

    @CBEPX

    Fixed in CBEPX/codex-plugin-cc v1.4.0 (https://github.com/CBEPX/codex-plugin-cc/releases/tag/v1.4.0): the Stop hook now reads stdin with a bounded 2-second deadline and a 1 MiB cap instead of a blocking readFileSync(0); when the review gate is disabled, a timed-out read with no data allows the turn immediately instead of waiting for the host's 900-second hook timeout; upstream PR #544 used as reference, thanks @stantheman0128.

    claude plugin marketplace add CBEPX/codex-plugin-cc && claude plugin install codex@cbepx
    

    This is a fork release, not an upstream fix, so I'm leaving this issue open for the maintainers here.

  2. added a commit that references this issue on Sep 28, 2026
  3. stantheman0128 commented on Sep 28, 2026

    @stantheman0128

    Thanks. #544 is the upstream change for this and it is still open. A fork release is useful, and leaving the issue open until that PR lands is right.

  4. Shinzor commented on Nov 3, 2026

    @Shinzor

    Current-source confirmation, checked November 4, 2026: main at db52e28 (plugin 1.0.6) still synchronously acquires stdin before checking the gate.

    This also affects the five-second lifecycle hooks, so a shared fix should cover both:

    Safe Windows/Node 26.13.0 probes wrote an incomplete JSON prefix and held stdin open for six seconds. Both scripts stayed alive until EOF, exiting at 6,030 ms each. They then rejected the malformed input with exit 1, before state cleanup or reviewer work. This tests the input phase only; no model or real broker operation ran.

    PR #544 is already open for the Stop path; this adds coverage requirements rather than another duplicate issue or patch:

    • Share a bounded asynchronous reader across Stop and lifecycle handlers, with a byte cap and a deadline below the shortest outer hook budget. A thirty-second input wait cannot fit a five-second SessionStart/SessionEnd budget.
    • Cover empty, partial and complete JSON while the producer stays open, oversized input, stream errors and malformed closed input.
    • Preserve payload.cwd selection before deciding the actual workspace's gate policy; a guessed environment/cwd must not bypass an enabled gate.
    • Verify listener/timer cleanup and actual process exit, not just promise resolution.
    • Keep the separate repeated-Stop guard tracked in Stop-review gate hook loops until CLAUDE_CODE_STOP_HOOK_BLOCK_CAP (missing stop_hook_active guard) #548.

    The held-open probe does not establish which handler caused any particular historical timeout warning.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions