TUI: first-run trust dialog drops the ENTER sent as soon as it renders (0.148.0, 0.149.0-alpha.1; 0.147.0 accepts it)
What happened
Since 0.148.0, an ENTER written to the pty as soon as the first-run
directory-trust screen renders (Press enter to continue) is dropped. The
screen stays up and the session never reaches the composer.
0.147.0 accepts the same keystroke at the same moment. 0.149.0-alpha.1
still drops it, so this is not fixed on the alpha line.
This breaks every non-interactive driver of the TUI — terminal-recording
harnesses, CI smoke tests, expect-style scripts — because "the marker is on
screen" is the only signal such a driver has that the dialog is ready for an
answer.
Reproduction
No wrapper, no config, no network: a bare codex in a pty under a throwawayHOME, with ENTER written the instant Press enter to continue first
appears.
"""Save as codex_trust_dialog_probe.py; run: python3 codex_trust_dialog_probe.py 0.148.0 enter-now"""
import fcntl, os, pty, re, select, struct, subprocess, sys, tempfile, termios, time
STRATEGIES = {
"enter-now": [(0.0, b"\r")],
"enter-after-3s": [(3.0, b"\r")],
}
MARKER = b"Press enter to continue"
version = sys.argv[1]
strategy = sys.argv[2] if len(sys.argv) > 2 else "enter-now"
prefix = f"/tmp/codex-versions/{version}"
binary = f"{prefix}/node_modules/.bin/codex"
if not os.path.exists(binary):
os.makedirs(prefix, exist_ok=True)
subprocess.run(["npm", "install", "--prefix", prefix, f"@openai/codex@{version}"], check=True)
home, work = tempfile.mkdtemp(), tempfile.mkdtemp()
pid, fd = pty.fork()
if pid == 0:
os.chdir(work)
os.environ["TERM"] = "xterm-256color"
os.environ["HOME"] = home
os.execv("/bin/sh", ["/bin/sh", "-c", binary])
# pty.fork() leaves the slave at 0x0 and codex renders nothing into it.
fcntl.ioctl(fd, termios.TIOCSWINSZ, struct.pack("HHHH", 30, 80, 0, 0))
pending = list(STRATEGIES[strategy])
buffer, marker_at, started = b"", None, time.time()
while time.time() - started < 20:
readable, _, _ = select.select([fd], [], [], 0.1)
if readable:
chunk = os.read(fd, 65536)
if not chunk:
break
buffer += chunk
if marker_at is None and MARKER in buffer:
marker_at = time.time()
if marker_at is not None and pending and time.time() - marker_at >= pending[0][0]:
os.write(fd, pending.pop(0)[1])
plain = re.sub(r"\x1b\[[0-9;?>< ]*[a-zA-Z]", "", buffer.decode("utf8", "replace")).replace("\x1b", "")
print(f"codex {version} strategy={strategy} bytes={len(buffer)} "
f"still_on_dialog={MARKER.decode() in plain[-600:]}")
os.kill(pid, 15)
Results
| version | keystroke | bytes written to the pty | still on the trust dialog after 20 s |
|---|---|---|---|
| 0.147.0 | ENTER at marker + 0 s | 5 986 | no |
| 0.147.0 | ENTER at marker + 3 s | 27 838 | no |
| 0.148.0 | ENTER at marker + 0 s | 141 221 | yes |
| 0.148.0 | ENTER at marker + 3 s | 28 445 | no |
| 0.149.0-alpha.1 | ENTER at marker + 0 s | 9 680 | yes |
| 0.149.0-alpha.1 | ENTER at marker + 3 s | 4 026 | no |
Two separate observations:
- The keystroke is lost. In every
still_on_dialog=yesrow the write to
the pty succeeded; the dialog simply did not advance. Delaying the same
single ENTER by 3 s is enough for it to be accepted, so the dialog is
painted before its key handler is attached rather than after.
- 0.148.0 additionally enters a repaint loop. 141 KB in 20 s against
28 KB for the same run that got past the dialog: after the swallowed
keystroke it redraws the full screen roughly every 80 ms indefinitely.
0.149.0-alpha.1 no longer does this (9.7 KB) but still drops the key.
Expected
A keystroke delivered after the trust screen has rendered is acted on,
whatever the delay. A TUI that paints a prompt is contracting to accept the
answer to it.
Workarounds we found
All of these clear the dialog on 0.148.0; each needs a wall-clock delay, which
is why none of them is a real fix for a driver that only watches output:
ENTER≥ 3 s after the markerENTERimmediately and again 3 s later1thenENTER, both ≥ 3 s after the markerDown,Up,ENTER, all ≥ 3 s after the marker
Note that waiting for the terminal to go idle is not a usable substitute:
the trust screen animates, emitting a full repaint about every 80 ms in both
0.147.0 and 0.148.0, so an idle window never opens.
Environment
- Linux 6.8.0 x86_64,
TERM=xterm-256color, 80×30 pty @openai/codexinstalled from npm withnpm install --prefix- clean
HOMEper run, no~/.codexstate, noconfig.toml - 0.147.0 vs 0.148.0 vs 0.149.0-alpha.1, back to back on the same machine
Impact for us
We pin @openai/codex@0.147.0 in CI while this is open; the pin is what keeps
our Codex end-to-end leg meaningful, since on 0.148.0 it fails before any
request reaches the server under test.