diff --git a/plugins/codex/scripts/session-lifecycle-hook.mjs b/plugins/codex/scripts/session-lifecycle-hook.mjs index 9655eae..7942c36 100644 --- a/plugins/codex/scripts/session-lifecycle-hook.mjs +++ b/plugins/codex/scripts/session-lifecycle-hook.mjs @@ -20,11 +20,42 @@ export const SESSION_ID_ENV = "CODEX_COMPANION_SESSION_ID"; const PLUGIN_DATA_ENV = "CLAUDE_PLUGIN_DATA"; function readHookInput() { - const raw = fs.readFileSync(0, "utf8").trim(); - if (!raw) { - return {}; + // Claude Code may invoke hooks with stdin as a non-blocking pipe, which + // causes read() to throw EAGAIN before EOF. We read stdin in chunks, + // accumulating across EAGAIN retries so partial reads are not lost. + // See: https://github.com/openai/codex-plugin-cc/issues/120 + const MAX_RETRIES = 20; + const RETRY_DELAY_MS = 10; + const sleepBuf = new Int32Array(new SharedArrayBuffer(4)); + const chunks = []; + const buf = Buffer.alloc(65536); + let eagainCount = 0; + + for (;;) { + let n; + try { + n = fs.readSync(0, buf, 0, buf.length, null); + } catch (err) { + if (err.code !== "EAGAIN") { + throw err; + } + if (eagainCount >= MAX_RETRIES) { + // Gracefully degrade: session_id won't be set in CLAUDE_ENV_FILE, + // but the hook succeeds instead of surfacing a startup error. + return {}; + } + eagainCount++; + Atomics.wait(sleepBuf, 0, 0, RETRY_DELAY_MS); + continue; + } + if (n === 0) { + break; // EOF + } + chunks.push(Buffer.from(buf.subarray(0, n))); } - return JSON.parse(raw); + + const raw = Buffer.concat(chunks).toString("utf8").trim(); + return raw ? JSON.parse(raw) : {}; } function shellEscape(value) { diff --git a/plugins/codex/scripts/stop-review-gate-hook.mjs b/plugins/codex/scripts/stop-review-gate-hook.mjs index c22edbd..4196d6e 100644 --- a/plugins/codex/scripts/stop-review-gate-hook.mjs +++ b/plugins/codex/scripts/stop-review-gate-hook.mjs @@ -19,11 +19,40 @@ const ROOT_DIR = path.resolve(SCRIPT_DIR, ".."); const STOP_REVIEW_TASK_MARKER = "Run a stop-gate review of the previous Claude turn."; function readHookInput() { - const raw = fs.readFileSync(0, "utf8").trim(); - if (!raw) { - return {}; + // Claude Code may invoke hooks with stdin as a non-blocking pipe, which + // causes read() to throw EAGAIN before EOF. We read stdin in chunks, + // accumulating across EAGAIN retries so partial reads are not lost. + // See: https://github.com/openai/codex-plugin-cc/issues/120 + const MAX_RETRIES = 20; + const RETRY_DELAY_MS = 10; + const sleepBuf = new Int32Array(new SharedArrayBuffer(4)); + const chunks = []; + const buf = Buffer.alloc(65536); + let eagainCount = 0; + + for (;;) { + let n; + try { + n = fs.readSync(0, buf, 0, buf.length, null); + } catch (err) { + if (err.code !== "EAGAIN") { + throw err; + } + if (eagainCount >= MAX_RETRIES) { + return {}; + } + eagainCount++; + Atomics.wait(sleepBuf, 0, 0, RETRY_DELAY_MS); + continue; + } + if (n === 0) { + break; // EOF + } + chunks.push(Buffer.from(buf.subarray(0, n))); } - return JSON.parse(raw); + + const raw = Buffer.concat(chunks).toString("utf8").trim(); + return raw ? JSON.parse(raw) : {}; } function emitDecision(payload) {