codex exec Hangs Forever and Returns Nothing
The identical command finishes in a minute when you type it and hangs for ten minutes at 0.07s of CPU when a script runs it. No output, no error, no timeout. Codex is blocked waiting on a terminal that your script never gave it.
Automation stuck and you can't tell why?
Book a rescue session — we find the block live on a screen-share. No cause found, no charge.
Bottom Line
- It is not slow. It is blocked. Codex is waiting on stdin from a TTY that does not exist.
- Fix for scripts and cron: append
</dev/null, or passstdin=DEVNULLinsubprocess. - Fix for detached background jobs: run it in the foreground instead, or under a pty. Redirecting stdin is not always enough once the job is fully detached.
- Diagnose with CPU time, not wall time. Frozen CPU with climbing elapsed time means blocked, not busy.
- Batches: sequential foreground calls beat one big backgrounded script.
How to Confirm It in Ten Seconds
Before changing anything, prove it is a block rather than slow work. Find the PID and look at CPU time:
pgrep -fl codex
ps -o pid,etime,time,stat,command -p <PID>
You are comparing two columns:
ETIME— how long the process has been alive.TIME— how much CPU it has actually consumed.
A model call doing real work accumulates CPU seconds steadily. The failure looks like this: ETIME at 10:32 and TIME at 0:00.07. Ten minutes elapsed, seven hundredths of a second of CPU used. That process is not thinking. It is parked on a blocking read.
The STAT column corroborates it — an S state means interruptible sleep, which is what waiting on input looks like.
Case One: Scripts, Cron, and CI
The common case. Codex expects an interactive terminal and tries to read from standard input. In a non-interactive shell there is no TTY attached, the read never returns, and nothing times out because nothing has gone wrong from the process’s point of view. It is patiently waiting for you.
Fix — shell:
codex exec "refactor the parser module" </dev/null
Fix — Python:
import subprocess
subprocess.run(
["codex", "exec", "refactor the parser module"],
stdin=subprocess.DEVNULL, # this is the fix
capture_output=True,
text=True,
timeout=900, # and always set one of these
)
The timeout is not optional in anything you schedule. Without it, one blocked call holds the entire job open until something else kills it, which on a nightly run means you find out in the morning.
Case Two: Fully Detached Background Jobs
There is a second, nastier variant. You background the whole thing — nohup ... &, a run_in_background helper, a detached worker — with output redirected to a file, and it stalls even though you handled stdin.
The observed shape: a backgrounded script wrapping codex exec sitting at about 0.07s of CPU for ten minutes with no output and no generated files, while the identical command run in the foreground completes in roughly a minute.
Closing stdin does not reliably rescue this one. What works:
- Run
codex execin the foreground. If the surrounding job needs to be scheduled, schedule the job; let Codex itself run in the foreground inside it. - Or give it a pty. If you genuinely must detach, wrap it so a pseudo-terminal exists:
script -q /dev/null codex exec "your prompt" </dev/null
- For batches, go sequential. Ten foreground calls in a loop are more reliable than one detached script doing ten things. Slower on paper, finishes in practice.
Why Wrapping It in a Timeout Is Not a Fix
The tempting move is:
timeout 300 codex exec "..." || echo "gave up"
That converts a hang into a failure, which is an improvement for your logs and no improvement at all for your output. You still get nothing done, now on a schedule. Use timeout as a backstop alongside the stdin fix, never instead of it.
The General Rule
This is one instance of a pattern worth internalizing, because every CLI-shaped agent tool has some version of it:
A command-line tool built for humans assumes a human is attached.
Interactive tools reach for stdin, check isatty(), emit ANSI escapes, prompt for confirmation, and open pagers. All of those behave differently or block outright when the terminal is gone. When any CLI works by hand and hangs in automation, the first question is not “what is slow” but “what is it waiting for.” The answer is usually input, and usually there is a flag or a redirect that says “nobody is coming.”
Related
- Claude Code Works in Your Terminal But Fails in Cron — the same “works by hand, fails scheduled” shape, different cause.
Tried both fixes and it still stalls?
Then it's the sandbox scope or the provider config, and those are harder to see. Book a rescue session.
Need OpenClaw fixed live?
Remote rescue sessions for gateway, auth, tunnel, VPS, and model access problems.
See Rescue Session