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)

Open 💬 0 comments Opened Aug 19, 2026 by konard

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 throwaway
HOME, 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:

  1. The keystroke is lost. In every still_on_dialog=yes row 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.

  1. 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 marker
  • ENTER immediately and again 3 s later
  • 1 then ENTER, both ≥ 3 s after the marker
  • Down, 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/codex installed from npm with npm install --prefix
  • clean HOME per run, no ~/.codex state, no config.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.

View original on GitHub ↗