Commands hang indefinitely when timeout occurs on shell-wrapped processes

Resolved 💬 11 comments Opened Sep 27, 2025 by danielchristiancazares Closed Nov 17, 2025
💡 Likely answer: A maintainer (etraut-openai, contributor) responded on this thread — see the highlighted reply below.

What version of Codex is running?

codex-cli 0.42.0

Which model were you using?

gpt-5-codex

What platform is your computer?

Linux 6.6.87.2-microsoft-standard-WSL2 x86_64 unknown

What steps can reproduce the bug?

When a shell tool call times out, Codex only kills the shell wrapper (bash -lc), not its child processes. These orphaned children keep stdout/stderr pipes open, even causing Codex to hang indefinitely in some cases despite the 366s wrapper timeout.

Steps to Reproduce

On any Linux machine:

  1. Launch Codex (CLI or desktop).
  2. Run (with a 5-second timeout): mkfifo pipe.$$; (while true; do echo child; sleep 1; done) > pipe.$$ & cat pipe.$$
  3. Wait for the five-second timeout. Codex logs that the tool call timed out, but the UI remains on "working".
  4. In another terminal, run ps -ef | grep '[p]ipe' (or inspect the loop). Both the background writer and cat are still running.

Observed Behavior

Even after the timeout, the child pipeline keeps running. Because those descendants keep the stdout pipe open, Codex's reader tasks never finish, so the turn remains stuck until you manually interrupt.

Expected Behavior

When a tool call times out, Codex should terminate the entire process tree so stdout and stderr close, the readers exit, and the tool call finishes cleanly.

Additional Notes

A real-world example is cargo test --release with a timeout. Cargo's child runners survive the timeout, continue emitting output, and Codex stays in "working" until you abort the turn.

Root Cause

  • Commands wrapped in bash -lc create a shell wrapper process.
  • On timeout, only the wrapper is killed via its PID.
  • Child processes become orphaned but keep running.
  • They hold stdout/stderr pipes open indefinitely.
  • Reader tasks block waiting for EOF that never arrives.

Fix

  • Use setsid() to create process groups.
  • Kill the entire process group with kill(-pid, signal).
  • Ensures all descendants terminate and pipes close properly.

View original on GitHub ↗

11 Comments

danielchristiancazares · 9 months ago

<img width="2403" height="561" alt="Image" src="https://github.com/user-attachments/assets/f2fdd559-55b1-4149-9a36-c8ebb771e03b" />
Associated codex-tui.log line:
2025-09-27T05:28:22.762846Z INFO FunctionCall: shell({"command":["bash","-lc","/usr/bin/python3 - <<'PY'\nimport
signal, time\nsignal.signal(signal.SIGHUP, signal.SIG_IGN)\nwhile True:\n print('child', flush=True)\n
time.sleep(1)\nPY"],"timeout_ms":2000,"workdir":"/root"}

ribelo · 9 months ago

The same thing happens on Linux, and I can't figure out why. I also looked at the code but found nothing. Not all commands hang and ignore the timeout, but the simplest one that does this is timeout 30 cargo test. If there is a deadlock in the code, the codex will hang despite the timeout. Always.

danielchristiancazares · 9 months ago
The same thing happens on Linux, and I can't figure out why. I also looked at the code but found nothing. Not all commands hang and ignore the timeout, but the simplest one that does this is timeout 30 cargo test. If there is a deadlock in the code, the codex will hang despite the timeout. Always.

I have a fix in place already for windows, linux and macOS. I just haven't pushed the branch.

aljungberg · 9 months ago

Agree with OP that any command launched should be handled as a process group. The agent should probably be taught about job management the way codex cloud seems to be (just my speculation but I see codex cloud seemingly make decisions about whether to wait longer or not for commands it launched).

For me codex waited over 10 hours on a process which, ironically, was an experiment with go process cancellation. I don't know if it was supposed to have a built in timeout but if it did, it was either never triggered or the action the timeout handler took was ineffective. From the session log (shortened to remove some irrelevant detail):

{"timestamp":"2025-09-28T22:07:31.421Z","type":"event_msg","payload":{"type":"agent_reasoning","text":"**Examining server shutdown flow and signal handling issues**\n\nI’m working through how the HTTP server shutdown and signal handling interplay—especially whether the server or manager close might block exit and why no graceful shutdown logs appear on ctrl-c. ... Testing with additional instrumentation and replicating signal receipt outside the container might help confirm the cause."}}
{"timestamp":"2025-09-29T08:20:26.595Z","type":"event_msg","payload":{"type":"turn_aborted","reason":"interrupted"}}

As you can see, I left it running over night and it never resolved. This was on codex-cli 0.41.0. Here's the actual command which may be useful for reproduction purposes:

• Ran
  └ cat <<'EOF' > /tmp/sigtest.go
    package main

    import (
        "context"
        "log"
        "os"
        "os/signal"
        "syscall"
        "time"
    )

    func main() {
        ctx, cancel := context.WithCancel(context.Background())
        defer cancel()
        sigCh := make(chan os.Signal, 1)
        signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
        defer signal.Stop(sigCh)
        shutdownComplete := make(chan struct{})
        go func() {
            sig := <-sigCh
            log.Printf("received %s, starting graceful shutdown", sig)
            cancel()
            select {
            case sig = <-sigCh:
                log.Printf("received %s before shutdown completed, forcing exit", sig)
                os.Exit(1)
            case <-shutdownComplete:
            }
        }()
        <-ctx.Done()
        log.Printf("ctx done")
        close(shutdownComplete)
        time.Sleep(100*time.Millisecond)
    }
    EOF

    go run /tmp/sigtest.go &
    PID=$!
    sleep 1
    kill -INT $PID
    wait $PID
sevdeutsch · 9 months ago

I have the same issue on various processes. MacOS. Claude Code has a default 2min timeout which is applied to all processes, which can be problematic sometimes but is significantly better then getting stuck indefinitely.

Would appreciate a quick fix soon & a good fix later.

raghavsethi · 9 months ago

Affecting several folks on our end as well. In our case the stuck script is:

pnpm concurrently \
  --names "lint,frmt,typecheck" \
  --prefix "[{name}]" \
  "pnpm lint" \
  "pnpm format" \
  "pnpm typecheck" 

This matches @danielchristiancazares thesis about pipes.

danielchristiancazares · 9 months ago

@jif-oai @bolinfest I've a fix solidified, but banging my head against a wall on how to properly unit test this without causing the same blocking behavior when/if regressions occur. Shall I open a draft PR for comments?

etraut-openai contributor · 8 months ago
raghavsethi · 8 months ago

Thank you @etraut-openai !!

bf4 · 8 months ago

Not sure if related, but I've found that sometimes commands can hang for hours without response on OsX"run pwd" to hang, and "run pwd with timeout_ms to 1,000 ms" to hang, but "run pwd without bash -lc" to return immediately. I can see the difference in the tui logs.

Intentionally adding this to a closed issue since I'm not sure it merits a new one and might help someone searching.

codex-cli 0.58.0 OSX, iTerm and TMux

danielchristiancazares · 8 months ago

@bf4 i can confirm they remediated this particular root cause. they do some nasty python jiggery pokery to spawn detached grandchildren but it works. im not too sure what your issue is. i'd recommend opening a new ticket if you can reproduce it.