codex exec "<prompt>" hangs indefinitely when stdin is a non-TTY pipe with no writer
Summary
When codex exec is invoked with a prompt as an argument from a non-interactive child shell (e.g. a CI runner, an editor's task runner, or another agent that spawns child processes), it hangs forever waiting for stdin EOF — even though the prompt was passed via the positional argument and stdin is never used.
This is a deterministic deadlock, not a flake. Reproducible on Windows 11 with codex-cli 0.128.0 and prior versions.
Repro
Inside a non-interactive child shell (here: a parent process spawning powershell.exe with piped stdio):
& codex exec "Reply OK only."
Hangs. Process has 0% CPU. No docker.exe MCP child spawned. No rollout
file written under ~/.codex/sessions. Stays hung until manually killed.
The same call with stdin redirected from NUL completes in ~11 s with normal output:
cmd /c "codex exec "Reply OK only." < NUL"
OK
tokens used: 19,542
Diagnosis
Stack inspection (no MCP child, 0 CPU, no rollout output, only stderr line Reading additional input from stdin...) plus --help text confirm that exec reads stdin when it is detected as a non-TTY pipe and appends the contents as a <stdin> block after the prompt argument:
If stdin is piped and a prompt is also provided, stdin is appended as a <stdin> block
In a non-TTY child shell with inherited but unwritten stdio, read() on stdin blocks indefinitely because the writer side is open but no bytes ever arrive and no EOF is delivered.
Why this matters
Standard for tools that spawn codex exec programmatically (Claude Code, GitHub Actions runners, nohup, Start-Process without -RedirectStandardInput NUL, agent orchestrators).
The codex app-server path (used by the Claude Code Codex plugin) is unaffected because the plugin actively writes JSONL and closes stdin.
Indistinguishable from a real hang/network problem from the caller's perspective — no logs, no rollout, no exit code, just silence.
Common mitigations (timeouts, retries) make the symptom worse: the next retry hangs the same way.
Proposed fix (one of)
When a prompt argument is present and stdin is a non-TTY pipe, only read stdin if data is actually available within a short timeout (e.g. poll for readability for 200 ms; if nothing, treat as empty and proceed).
Add an explicit --no-stdin flag (or invert the default to require - for stdin reads), so non-interactive callers can opt out cleanly.
Document the failure mode and the workaround prominently in codex exec --help, and emit a stderr warning after, say, 5 s of waiting so the deadlock is at least detectable.
Option 1 is the friendliest default and matches what most callers expect when they pass a prompt argument. Option 2 is the smallest behavior change.
Workaround (current)
POSIX shells: codex exec "<prompt>" < /dev/null
Windows cmd: codex exec "<prompt>" < NUL
Windows PowerShell child: cmd /c "codex exec "<prompt>" < NUL" (PowerShell has no clean pipe-to-NUL syntax)
Or wrap once: a codex-safe.cmd shim that forwards args and redirects from NUL.
Environment
Windows 11 26200 (Home)
codex-cli 0.128.0 (npm install path: C:\Users\<u>\AppData\Roaming\npm\codex.cmd → node_modules\@openai\codex-win32-x64\vendor\x86_64-pc-windows-msvc\codex\codex.exe)
Reproduces from PowerShell child invoked by Claude Code's Bash tool (PowerShell runs & codex exec "<prompt>" with inherited piped stdio)
Also reproduces from Start-Process powershell -ArgumentList ... without -RedirectStandardInput NUL
3 Comments
Potential duplicates detected. Please review them and close your issue if it is a duplicate.
Powered by Codex Action
I validated this on current
mainand prepared a minimal branch: https://github.com/mitre88/codex/tree/fix/exec-optional-stdin-ready-checkThe change keeps forced stdin behavior unchanged (
codex exec -and no positional prompt still read stdin). When a positional prompt is present, optional stdin append now waits up to 200 ms for pipe data and proceeds without a<stdin>block if none arrives, matching the suggested option 1. It still appends immediately available piped input.Added coverage:
codex-execan open stdin pipe with no data and asserts the request contains only the positional prompt.Validation:
cargo fmt --package codex-exec --checkgit diff --checkcargo test -p codex-exec stdin_fd_has_data_within -- --nocapturecargo test -p codex-exec exec_with_prompt_does_not_wait_for_empty_open_stdin_pipe --test all -- --nocapturecargo test -p codex-execI also tried
cargo check --target x86_64-pc-windows-msvc -p codex-exec, but this local machine does not have that Rust target installed (can't find crate for core). I can open the PR if maintainers want this branch.I refreshed the branch against current
mainand revalidated the focused coverage.Updated branch: https://github.com/mitre88/codex/tree/fix/exec-optional-stdin-ready-check
Compare: https://github.com/openai/codex/compare/main...mitre88:codex:fix/exec-optional-stdin-ready-check
Validation after rebase:
cargo fmt --package codex-exec --checkcargo test -p codex-exec stdin_fd_has_data_within -- --nocapturecargo test -p codex-exec exec_with_prompt_does_not_wait_for_empty_open_stdin_pipe --test all -- --nocapturegit diff --checkThe scope is still the same: forced stdin remains unchanged, while optional stdin alongside a positional prompt only appends when data is ready.