unified_exec leaks tty file descriptors

Resolved 💬 4 comments Opened Nov 24, 2025 by steipete Closed Nov 24, 2025

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

  1. 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.

  1. (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.

  1. 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_

View original on GitHub ↗

This issue has 4 comments on GitHub. Read the full discussion on GitHub ↗