Windows native TUI inserts [C/[D/[3~ when VT input is re-enabled
What version of Codex CLI is running?
codex-cli 0.140.0-alpha.19 from the Windows npm package.
I also reproduced this on a native Windows source build from openai/codex@dfd03ea01b before applying a local diagnostic patch.
This appears to be a recent regression in the native Windows path: the same Windows environment was usable with the previous stable npm version before updating.
What subscription do you have?
Not included here. The issue reproduces in local TUI input handling before any model/API request is needed.
Which model were you using?
Not model-specific.
What platform is your computer?
Microsoft Windows NT 10.0.26200.0 x64
Additional context from codex doctor --json: Windows 11 Professional, x64.
WSL2 Fedora in the same Windows Terminal does not reproduce the issue.
What terminal emulator and version are you using (if applicable)?
Primary repro: Windows Terminal 1.24.11321.0 with PowerShell 7.6.2 (pwsh).
Also reproduced on native Windows in Git Bash and VS Code integrated terminal. No tmux/screen/zellij.
Codex doctor report
Redacted to the parts relevant to this TUI input issue:
{
"schemaVersion": 1,
"overallStatus": "ok",
"codexVersion": "0.140.0-alpha.19",
"checks": {
"installation": {
"status": "ok",
"summary": "installation looks consistent",
"details": {
"install context": "npm on windows-x86_64"
}
},
"runtime.provenance": {
"status": "ok",
"summary": "running npm on windows-x86_64",
"details": {
"platform": "windows-x86_64",
"version": "0.140.0-alpha.19"
}
},
"system.environment": {
"status": "ok",
"summary": "Windows 11 Professional [64-bit]",
"details": {
"os": "Windows 10.0.26200 (Windows 11 Professional) [64-bit]",
"os type": "Windows",
"os version": "10.0.26200"
}
},
"terminal.env": {
"status": "ok",
"summary": "terminal metadata was detected",
"details": {
"terminal": "Windows Terminal",
"terminal size": "120x50"
}
}
}
}
What issue are you seeing?
I am filing this as a native Windows TUI input regression report, with a locally validated mitigation included below for context.
On native Windows terminals, the Codex TUI can intermittently insert terminal escape-sequence tails into the composer when navigation/editing keys are pressed. For example, pressing Right/Left/Down/Delete can leave literal text like [C, [D, [B, and [3~ in the input area.
This materially degrades usability of the native Windows TUI. The same workflow in WSL2 inside Windows Terminal does not show the issue.
What steps can reproduce the bug?
- Start native Windows
codexin Windows Terminal + PowerShell. - Focus the composer.
- Press arrow keys and Delete.
- Watch the composer text.
I could also reproduce this with constructed PTY input in both WinPTY and ConPTY paths by sending focus + navigation/delete sequences equivalent to:
ESC[O ESC[I ESC[C ESC[C ESC[C ESC[D ESC[D ESC[B ESC[3~ XYZ
On the affected build, the visible composer text contains escape tails such as [C, [D, [B, and [3~.
What is the expected behavior?
Arrow keys and Delete should be handled as key events/navigation/editing operations. Their escape-sequence bytes should not be inserted as text in the composer.
Additional information
I instrumented a local native Windows build to narrow this down. The evidence points to ENABLE_VIRTUAL_TERMINAL_INPUT being enabled on the console input handle while crossterm's Windows event stream is reading.
A representative failing state was:
input_mode=0x00000398, ENABLE_VIRTUAL_TERMINAL_INPUT=true
raw crossterm events: KeyEvent { code: Char('[') }, KeyEvent { code: Char('C') }, ...
After clearing ENABLE_VIRTUAL_TERMINAL_INPUT, the mode becomes 0x00000198 and the same navigation/delete input no longer leaks [C/[D/[B/[3~ into the composer.
The notable detail is that clearing this bit only during initial setup was not sufficient in this environment. Logging showed startup and event-stream creation with VT input disabled, followed by a later Poll::Pending path where the input mode had returned to 0x398.
A locally validated mitigation does the following:
- Does not emit crossterm focus-change enable/disable commands on Windows.
- Defaults keyboard enhancement mode off on native Windows, while keeping the existing env override.
- Explicitly clears
ENABLE_VIRTUAL_TERMINAL_INPUTafter raw mode setup, after Windows color probing/palette probing, after crosstermEventStream::new(), and again when the event stream returnsPoll::Pendingbefore waiting for more stdin.
Validation of that local patch:
- Windows-side
cargo fmt --check: passed (only the existing rustfmt nightly-option warning). cargo test -p codex-tui tui::keyboard_modes --lib: passed, 11 tests.cargo check -p codex-tui: passed.cargo build -p codex-cli --bin codex: passed.- WinPTY/ConPTY constructed-input probes no longer leave
[C/[D/[B/[3~visible in the composer.
I would be happy to open a PR with this Windows-gated patch if maintainers consider this direction appropriate. I am also happy to adjust the shape of the change if the preferred boundary is closer to the crossterm event-source layer.
4 Comments
For completeness, here is the shape of the local mitigation that stopped the issue from reproducing in my Windows environment.
Diagnosis path
The visible
[C/[D/[B/[3~text was not coming from the composer itself. It was already present in the crossterm events as ordinary character events.With temporary logging around the Windows console input mode and raw crossterm events, a failing run showed the input handle in this state while the TUI was reading:
When the same input was read with
ENABLE_VIRTUAL_TERMINAL_INPUTcleared, the mode was:and crossterm produced normal key events instead of character events for the escape-sequence bytes.
The important detail is that clearing it only during initial TUI setup was not enough in this environment. Logging showed startup and
EventStream::new()with VT input already off, then a later event-stream wait path saw it back on:The local patch therefore treats
ENABLE_VIRTUAL_TERMINAL_INPUTas a mode bit that may be restored while the event stream remains alive, rather than as a one-time initialization concern.Shape of the local patch
The local patch has three parts.
1. Add a small Windows-only helper that clears VT input
In
codex-rs/tui/src/tui.rs:This leaves output VT processing alone. It only clears the input-side VT bit on
STD_INPUT_HANDLE, and treats missing/non-console handles as no-op.2. Clear VT input at every place that can hand control to crossterm's Windows reader
I added
let _ = disable_virtual_terminal_input();on Windows after:enable_raw_mode()inset_modes()probe_windows_default_colors()ininit()terminal_palette::default_colors()inTui::new()crossterm::event::EventStream::new()inCrosstermEventSource::default()The final call site was the one that made the repro stop reproducing locally:
Without the
Poll::Pendingguard, I could still observe VT input being off at initialization but on again before the next meaningful key read.3. Avoid Windows input features that may introduce VT-style input sequences
I made two conservative Windows-specific changes:
EnableFocusChange/DisableFocusChangeon Windows.The keyboard enhancement helper now effectively does:
The existing override still works, so
CODEX_TUI_DISABLE_KEYBOARD_ENHANCEMENT=0can re-enable it for experiments.Rationale
The rationale is that crossterm's Windows backend expects console input records for these navigation/editing keys. When
ENABLE_VIRTUAL_TERMINAL_INPUTis set on the input handle, the console may instead surface VT bytes such asESC [ C.In the failing path, crossterm then reports the tail bytes as
Char('[')andChar('C'), and the composer correctly inserts them as typed text. The local mitigation keeps the input handle in the console-record mode expected by that backend whenever the TUI is about to read from crossterm.Validation
With the local patch:
pwshno longer inserts[C/[D/[B/[3~when using arrows/Delete.cargo fmt --checkpassed, except for the existing rustfmt warning aboutimports_granularity = Item.cargo test -p codex-tui tui::keyboard_modes --libpassed, 11 tests.cargo check -p codex-tuipassed.cargo build -p codex-cli --bin codexpassed.I have kept the local patch narrow and Windows-gated. I can send this as a PR if maintainers would like to evaluate this approach, or split the change differently if the preferred fix should live closer to the crossterm event-source boundary.
I also encountered a similar issue! Rollback to something before
0.140.0like0.139.0might works.Same problem happens in 0.139.0
I can add a deterministic trigger and a concrete root-cause path for the same
ENABLE_VIRTUAL_TERMINAL_INPUTfailure described here.Environment
10.0.22000)codex-cli 0.145.02.7.11.0Observed incident
In a long-running native Windows Codex session, the console input mode was healthy at
0x1F0, then became stuck at0x3D8after batched parallelwsl.exetool calls. At that point:[A,[B,[C, or[D;[3~;Restoring the same live console input handle from
0x3D8to0x1F0immediately repaired key handling without restarting or resuming Codex. The relevant bit for the escape-tail symptom isENABLE_VIRTUAL_TERMINAL_INPUT(0x200), matching this issue.Deterministic isolated reproduction
I reproduced the mode corruption outside the user's active session in a disposable native Windows console:
CONIN$mode:0x1F7.wsl.exe -- sh -c "sleep 2".wsl.exe -- sh -c "sleep 5"in the same console.CONIN$mode:0x3D8, withENABLE_VIRTUAL_TERMINAL_INPUTstill set.The exit-order timeline is:
Controls:
wsl.exe -- truecalls all restored the original mode.This explains why the problem appears and disappears intermittently: the final console mode depends on which overlapping WSL client exits last.
Root-cause path
WSL 2.7.11 opens the shared
CONIN$, snapshots its current mode, enablesENABLE_WINDOW_INPUT | ENABLE_VIRTUAL_TERMINAL_INPUT, and clears several other input flags:https://github.com/microsoft/WSL/blob/2.7.11/src/windows/common/ConsoleState.cpp#L65-L86
Each WSL client later restores its own snapshot unconditionally in its destructor:
https://github.com/microsoft/WSL/blob/2.7.11/src/windows/common/ConsoleState.cpp#L118-L135
With overlapping clients, B can snapshot A's temporary mode and restore that temporary mode after A has already restored the original mode.
Codex then remains broken because its Windows raw-mode path does not defensively remove a later reintroduced
ENABLE_VIRTUAL_TERMINAL_INPUTbit. That is consistent with the instrumentation and mitigation already reported in this issue.Suggested mitigation in Codex
Any of these boundaries would prevent a WSL-side mode race from permanently breaking the TUI:
ENABLE_VIRTUAL_TERMINAL_INPUTwhen using the Windows event API.Companion WSL report: https://github.com/microsoft/WSL/issues/41201
No private session transcript or project paths are included; the reproduction above is standalone.