Skip to content

fix: handle EAGAIN in hook scripts when stdin is non-blocking - #165

Open
ikbear wants to merge 3 commits into
openai:mainfrom
ikbear:fix/eagain-hook-stdin
Open

ikbear wants to merge 3 commits into
openai:mainfrom
ikbear:fix/eagain-hook-stdin

Conversation

@ikbear

@ikbear ikbear commented Apr 7, 2026

Copy link
Copy Markdown

Problem

Claude Code invokes hooks with stdin as a non-blocking pipe on macOS and Linux. This causes fs.readFileSync(0) in readHookInput() to throw EAGAIN: resource temporarily unavailable, making the hook exit with code 1. The result is a SessionStart:startup hook error banner shown on every session start.

Reported in #120.

Root cause

function readHookInput() {
  const raw = fs.readFileSync(0, "utf8").trim(); // throws EAGAIN on non-blocking stdin
  ...
}

Fix

Retry readFileSync up to 20 times (200ms total) when EAGAIN is received, using Atomics.wait for synchronous sleeping. If all retries are exhausted, returns {} so the hook exits cleanly (graceful degradation) instead of surfacing a startup error.

const MAX_RETRIES = 20;
const RETRY_DELAY_MS = 10;
const sleepBuf = new Int32Array(new SharedArrayBuffer(4));

for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
  try {
    const raw = fs.readFileSync(0, "utf8").trim();
    return raw ? JSON.parse(raw) : {};
  } catch (err) {
    if (err.code !== "EAGAIN") throw err;
    if (attempt === MAX_RETRIES) return {};
    Atomics.wait(sleepBuf, 0, 0, RETRY_DELAY_MS);
  }
}

Applied to both session-lifecycle-hook.mjs and stop-review-gate-hook.mjs.

Relation to PR #123

PR #123 addresses the same issue with a more complex buffered approach. This PR offers a simpler implementation with the same intent.

Fixes #120

Claude Code invokes hooks with stdin as a non-blocking pipe on some
platforms (macOS, Linux). This causes fs.readFileSync(0) to throw
EAGAIN: resource temporarily unavailable, making the hook exit with
code 1 and showing a SessionStart:startup hook error banner on every
session start.

Fix by retrying readFileSync up to 20 times (200ms total) when EAGAIN
is received, using Atomics.wait for precise synchronous sleeping. If
all retries are exhausted, returns {} so the hook succeeds gracefully
rather than surfacing a startup error.

Fixes openai#120
@ikbear
ikbear requested a review from a team April 7, 2026 03:15

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 7927327b87

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

…l reads

When stdin arrives in multiple writes on a non-blocking pipe, readFileSync
can consume a partial chunk internally before throwing EAGAIN, causing the
already-read bytes to be silently discarded. On the next retry, readFileSync
starts from an empty buffer, so the accumulated bytes are gone and JSON.parse
fails on the tail chunk alone.

Switch to readSync in a manual accumulation loop: each successful read appends
to a chunks array, EAGAIN causes a wait-and-retry without touching the
accumulated data, and EOF triggers a single Buffer.concat + JSON.parse.

Fixes the review comment on openai#165.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c2d931d06d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

When the retry budget is exhausted after partial bytes have already been
read, breaking out of the loop and falling through to JSON.parse risks
throwing SyntaxError on truncated JSON, which fails the hook instead of
degrading gracefully. Return {} directly on max retries to guarantee
clean hook exit regardless of how much data was buffered.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b51bc28408

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

fireblue added a commit to fireblue/codex-plugin-cc that referenced this pull request May 17, 2026
…penai#255, openai#165, openai#253, openai#209, openai#239, openai#307, openai#315)

- Raise adversarial review inline diff limits to 50 files / 1MB (openai#327)
- Add wall-clock timeouts to JSON-RPC requests (openai#302)
- Use codex-namespaced clientInfo.name (openai#200, openai#255)
- Handle EAGAIN in hook scripts for non-blocking stdin (openai#165)
- Replace Node.js SessionStart hook with faster bash script (openai#253)
- Add --fast flag for service_tier support (openai#209)
- Set timeout: 600000 on foreground review/adversarial-review flows (openai#239)
- Rename gpt-5-4-prompting skill to codex-prompting (openai#307)

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
CBEPX added a commit to CBEPX/codex-plugin-cc that referenced this pull request Sep 28, 2026
…eview prompt via stdin

New lib/hook-input.mjs readHookInput(): streams stdin with a deadline (2 s
default, CODEX_HOOK_STDIN_TIMEOUT_MS for tests), a 1 MiB byte limit and a
StringDecoder across chunk boundaries. EOF parses the whole input; at the
deadline a buffer that is already a complete JSON object is accepted (only
the EOF was late), anything else is `timeout`; over the limit is `overflow`
without parsing; bad JSON is `invalid-json`. Listeners and the timer are
removed and stdin destroyed, so the hook exits right after deciding.

Stop hook: any read error blocks as before (fail-closed), except a timeout
with nothing received while the workspace gate (CLAUDE_PROJECT_DIR / cwd) is
off, which allows (openai#530/openai#544). With the gate on, a timeout blocks naming
"hook input did not arrive". The review prompt now reaches the companion via
`task --prompt-stdin`, not argv, lifting the Windows/Linux argv limits.

SessionStart/SessionEnd hook: reads with a 1 s deadline before the 12 s
SessionEnd budget starts; on any read error it prints one stderr line and
returns without cleanup. The hooks.json timeout test now asserts 15 s >
budget + read.

readStdinIfPiped (companion): readSync loop that keeps accumulated bytes
across EAGAIN on a non-blocking stdin (up to 50 x 20 ms consecutive) and
throws on exhaustion instead of returning a partial prompt
(openai#120/openai#247/openai#123/openai#150/openai#165).

Co-authored-by: stantheman0128 <[email protected]>
Co-authored-by: tmchow <[email protected]>
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

EAGAIN crash in hook scripts: readFileSync(0) fails when stdin is non-blocking

2 participants