Baobaobaolin.com
date
entry
025
topic
tooling
rev
—

codex exec hangs: it is waiting for EOF on stdin

The same codex exec line returns in 11 seconds when stdin ends immediately, and hangs when another program calls it. It is waiting for the write end of stdin to close, and nobody is going to close it.

I often use Codex's command line, codex exec, as a second-pass checker: hand it a prompt, it reads files and returns a report. A short prompt asking it to reply "OK" comes back in 11 seconds when stdin ends immediately.

Call the same line from another program and it can hang indefinitely: no reply on stdout, and stderr stuck on one line about reading input. This note is about what it is waiting for: EOF on stdin. It covers three things: the mechanism, the situations that trigger it (one of which doesn't hang at all, but quietly eats someone else's data), and how to switch it off.

The mechanism: besides the prompt, it reads stdin

From codex exec --help (codex-cli 0.159.2), on the PROMPT argument:

If stdin is piped and a prompt is also provided, stdin is appended as a <stdin> block

So when a prompt is given and stdin is not a terminal, it reads stdin until EOF and appends whatever it got to the prompt (with no prompt, or with -, stdin is the whole prompt). Easy to confirm:

$ echo "The secret word is PAPAYA." | codex exec --skip-git-repo-check -s read-only \
    "What is the secret word? Reply with the word only."
PAPAYA

In the stderr log, the prompt has an extra block:

<stdin>
The secret word is PAPAYA.
</stdin>

That is a useful feature. The trouble is "until EOF": as long as the write end of the pipe stays open, the read end never reaches EOF, even if not a single byte is written. stderr shows Reading additional input from stdin..., and that is where it stays.

Case one: while the pipe is open, it waits

Same prompt (Reply with exactly: OK), four kinds of stdin, one run each, timed from start to exit:

stdinResult
< /dev/null11 s, replied OK
sleep 30 | (pipe open for 30 s)38 s, replied OK
Node.js spawn(), stdin left openstill running at 60 s, empty stdout; killed by the script
Node.js spawn(), then child.stdin.end()9.6 s, replied OK

The second row took 27 seconds longer than the first, which fits "it waited for the upstream 30-second sleep to end and close the pipe"; a single run's total also includes startup and model time, so the exact wait can't be separated out. The third row is how it gets stuck when called from code: the Node.js docs state that options.stdio for spawn() defaults to 'pipe', so stdin, stdout and stderr each get a pipe between parent and child. While the parent is alive and hasn't ended its end of stdin, the child never sees EOF. Agent tools, editor plugins and CI runners start subprocesses from code, and what stdin they pass depends on how they were written. You can't tell from the command line alone.

The reproduction (save as repro.mjs; node repro.mjs leaves stdin open and was still at the reading line after 45 s; node repro.mjs end closes it and printed OK in 11 s):

import { spawn } from "node:child_process";

const child = spawn("codex", ["exec", "--skip-git-repo-check", "-s", "read-only",
                              "Reply with exactly: OK"],
                    { stdio: ["pipe", "inherit", "inherit"] });
if (process.argv[2] === "end") child.stdin.end();
child.on("close", (code) => { process.exitCode = code ?? 1; });

Case two: it doesn't hang, it eats the loop's input

When stdin has data and does end, it doesn't hang; it takes everything. Inside a while read loop that is a different problem. Three slugs in a file, handed to codex one at a time:

$ printf 'alpha\nbeta\ngamma\n' > slugs.txt
$ n=0; while read -r slug; do n=$((n+1)); echo "iteration $n: $slug"
    codex exec --skip-git-repo-check -s read-only -o "D$n.last" \
      "Reply with exactly the word: $slug" > "D$n.out" 2> "D$n.err"
    echo "  codex said: $(cat "D$n.last")"
  done < slugs.txt; echo "loop ran $n times"
iteration 1: alpha
  codex said: alpha
loop ran 1 times

The loop ran once. Codex's log says exactly where the other two lines went:

<stdin>
beta
gamma
</stdin>

The codex call inside the loop has no stdin of its own, so it inherits the loop's stdin: the rest of slugs.txt. Codex reads it all as an attachment to the prompt, the next read hits EOF, and the loop ends, with no error message. Scale that to a list of a hundred articles (not tested) and the loop still calls codex once, then ends normally and looks finished.

ssh has the same kind of trap. man ssh describes -n as "Redirects stdin from /dev/null (actually, prevents reading from stdin)" and says it "must be used when ssh is run in the background".

The fix: give it a stdin that ends immediately

Same loop, with < /dev/null on the codex line and one output line per iteration:

$ n=0; while read -r slug; do n=$((n+1))
    codex exec --skip-git-repo-check -s read-only -o "E$n.last" \
      "Reply with exactly the word: $slug" < /dev/null > "E$n.out" 2> "E$n.err"
    echo "iteration $n: $slug -> $(cat "E$n.last")"
  done < slugs.txt; echo "loop ran $n times"
iteration 1: alpha -> alpha
iteration 2: beta -> beta
iteration 3: gamma -> gamma
loop ran 3 times

From code, the equivalents are:

  • Node.js: spawn(cmd, args, { stdio: ["ignore", "pipe", "pipe"] }) (stdin attached to /dev/null), or, with the default pipe, call child.stdin.end() right after starting it
  • Python: subprocess.run(..., stdin=subprocess.DEVNULL)

I now put < /dev/null in every codex exec template, including the ones I give to agents. When the prompt is fully passed as an argument and the task needs no stdin, whether it hangs waiting for EOF depends on the caller's stdin, and when you write the command you don't always know who the caller will be.

If you remember one thing

"Nothing was written in" and "stdin has reached EOF" are different things. The codex exec that hung at the top was waiting for a write end to close, and nobody was going to close it. Any CLI whose docs say "if stdin is piped" deserves a < /dev/null when you call it from a script and the task doesn't need stdin.

Revision history

  1. First published