unified_exec leaks tty file descriptors
What version of Codex is running?
0.63.0
What subscription do you have?
Pro
Which model were you using?
codex-max high
What platform is your computer?
Darwin 25.1.0 arm64 arm
What issue are you seeing?
unified_exec leaks tty file descriptors.
I know it's in beta but since this is shared widely on Twitter and I didn't find an issue I wanted to share my findings.
Eventually any command that opens a tty will fail.
I switched it back off and the issue's gone.
Here's an unverified trace of the issue from codex:
- exec_command always stores the session in the manager, even when the child has already
exited. See codex-rs/core/src/unified_exec/session_manager.rs:101-145: it calls
store_session(...) unconditionally, then sets session_id = None if has_exited is true.
Because the entry stays in self.sessions, the ExecCommandSession never drops, so its
PTY master/slave FDs stay open.
- ExecCommandSession’s Drop is what kills the child and closes the PTY (codex-rs/utils/
ptmx handles (lsof /dev/ptmx earlier showed 30–40 open FDs per Codex PID). Short-lived
exec calls (which are common with streaming terminal output) will all be “dead” but
still retained, so the PTY count climbs until the process is restarted.
Why the leak shows up more with “streaming terminal”
- Streaming mode issues many small execs and returns immediately once output is
collected, so nearly every call hits the has_exited fast path—each one leaves a PTY in
the map.
- Non‑streaming flows that keep a session alive are later cleaned up by
refresh_session_state when the user writes stdin or polls; the already-dead sessions
never get that cleanup.
Quick fix outline
- Don’t store finished sessions. In exec_command, gate store_session behind if !
has_exited { … }, and drop the session immediately when has_exited is true so Drop
closes the PTY.
- (Optional hardening) Add a lightweight GC: after collect_output_until_deadline, if
session.has_exited(), skip insertion; if session_id is None, ensure the session is
dropped before returning. Also consider a periodic sweep to remove any exited sessions
that were left alive in long-running interactions.
- Add a regression test: spawn a trivial command, assert sessions.len() stays 0 when
has_exited is true, and maybe count open PTYs before/after in a Unix-only test.
What steps can reproduce the bug?
Enable streamable_shell in config.toml
Use a dozen of codex instances and lots of debugging, anything that involes long-running cli commands where unified_exec is invoked a lot.
What is the expected behavior?
_No response_
Additional information
_No response_
This issue has 4 comments on GitHub. Read the full discussion on GitHub ↗