VS Code integrated terminal over SSH leaks kitty keyboard protocol sequences after exiting Codex (e.g. 9;1:3u)

Open 💬 7 comments Opened May 22, 2026 by Wunrry
💡 Likely answer: A maintainer (github-actions[bot], contributor) responded on this thread — see the highlighted reply below.

What version of Codex is running?

codex-cli 0.133.0

What platform is your computer?

Local machine:

  • VS Code integrated terminal over Remote SSH
  • VS Code terminal setting: terminal.integrated.enableKittyKeyboardProtocol = true
  • TERM_PROGRAM_VERSION=1.121.0

Remote machine:

  • Debian GNU/Linux 13 (trixie)
  • Linux 6.12.88+deb13-amd64 x86_64
  • Shell: bash
  • TERM=xterm-256color
  • TERM_PROGRAM=vscode

What steps can reproduce the bug?

  1. Open a fresh VS Code integrated terminal connected to a Linux host via SSH.
  2. Start codex.
  3. Exit codex.
  4. Press Tab in the shell.

This reproduces consistently in a fresh SSH session.

What is the expected behavior?

After exiting Codex, the shell should return to normal terminal input behavior and pressing keys like Tab should not print raw escape-sequence fragments.

What do you see instead?

After exiting Codex, the shell prints raw fragments such as:

9;1:3u

Sometimes I also see:

9;5:3u

These appear to correspond to kitty keyboard protocol key release events leaking into the shell instead of being fully cleaned up on exit.

Additional information

  • This does not appear to be a VPS locale/encoding problem.
  • The issue disappears if I disable the local VS Code setting:
"terminal.integrated.enableKittyKeyboardProtocol": false
  • I tested this in a fresh SSH session and can reproduce it reliably by entering and exiting Codex.
  • I also tested a Bash-side workaround that tried to reset keyboard enhancement flags on each prompt, but it did not reliably fix the problem in VS Code's integrated terminal.
  • This suggests an interoperability issue between Codex terminal cleanup on exit and VS Code/xterm.js kitty keyboard protocol handling.

Minimal environment evidence

codex-cli 0.133.0
Debian GNU/Linux 13 (trixie)
Linux 6.12.88+deb13-amd64 x86_64 GNU/Linux
TERM=xterm-256color
TERM_PROGRAM=vscode
TERM_PROGRAM_VERSION=1.121.0

View original on GitHub ↗

7 Comments

github-actions[bot] contributor · 1 month ago

Potential duplicates detected. Please review them and close your issue if it is a duplicate.

  • #22839

Powered by Codex Action

pomelohan · 29 days ago

I am seeing the same class of leaked keyboard-protocol bytes, but outside VS Code/SSH.

Environment

  • Codex CLI: codex-cli 0.141.0
  • Install method: Homebrew
  • Platform: macOS / Darwin 25.5.0 arm64 arm
  • Terminal: Ghostty 1.3.1
  • TERM=xterm-ghostty
  • TERM_PROGRAM=ghostty
  • No tmux or zellij detected
  • Auth mode: API key
  • Model in config: gpt-5.5

Symptom

After pressing Ctrl+C to terminate/exit Codex in the TUI, control returns to the parent shell and the shell sometimes displays:

9;5:3u

This appears after Codex has already exited, so it looks like pending enhanced keyboard / CSI-u bytes are reaching the shell.

Reproduction

  1. Open Ghostty 1.3.1 on macOS.
  2. Start codex.
  3. Press Ctrl+C to terminate/exit the TUI.
  4. Observe that literal characters like 9;5:3u may appear at the shell prompt after Codex exits.

Additional note

This makes the issue look broader than VS Code's integrated terminal. The :3u suffix is consistent with key release events in enhanced keyboard reporting. A plausible mechanism is that Codex exits on the Ctrl+C press event while the terminal later emits the corresponding release event, and that release event arrives after Codex has restored terminal modes and stopped reading input.

booxter · 21 days ago

I can reproduce what looks like the same issue in native kitty, not VS Code or another terminal wrapper.

Environment:

codex-cli 0.142.0
kitty 0.47.0
zsh 5.9
TERM=xterm-kitty
zsh vi mode enabled via bindkey -v

This happens when exiting Codex with Ctrl-C.

In my shell it presents as:

execute: 3u_

<img width="732" height="306" alt="Image" src="https://github.com/user-attachments/assets/8c79247a-c0d6-42be-a116-9cca7f18ff7c" />

The execute: part is likely zsh vi mode interpreting : as execute-named-cmd, so this may be the same leaked :3u / CSI-u key-release fragment reported here, just rendered differently by zsh vi mode.

probecat · 15 days ago

I can reproduce the same leaked keyboard-protocol sequence outside VS Code/SSH.

Environment:

  • Codex CLI: codex-cli 0.142.5
  • Terminal: Alacritty 0.17.0
  • OS: Arch Linux
  • Shell: zsh
  • Not inside tmux/zellij

Symptom after Codex exits:

  • Ctrl+D leaves/prints 0;1:3u
  • Ctrl+C leaves/prints 9;5:3u

Workaround confirmed: launching with CODEX_TUI_DISABLE_KEYBOARD_ENHANCEMENT=1 codex fixes it for me.

samuel-gamelin · 8 days ago

I can reproduce this on native iTerm2, without SSH or a terminal multiplexer.

Environment

  • Codex CLI: 0.144.1
  • Install: Homebrew cask
  • macOS: 26.5.2 (25F84), arm64
  • Terminal: iTerm2 3.6.11
  • Shell: zsh 5.9
  • TERM=xterm-256color
  • TERM_PROGRAM=iTerm.app
  • TERM_PROGRAM_VERSION=3.6.11
  • COLORTERM=truecolor
  • LANG=en_CA.UTF-8
  • LC_ALL=C.UTF-8
  • LC_CTYPE=C.UTF-8
  • No tmux, screen, zellij, or SSH
  • CODEX_TUI_DISABLE_KEYBOARD_ENHANCEMENT is unset

Observed behavior

At the idle Codex prompt:

  • /quit and /exit exit cleanly.
  • Ctrl+C sometimes exits and leaves 9;5:3u at the zsh prompt.
  • Ctrl+D sometimes exits and leaves 00;5u00;5:3u at the zsh prompt.

These appear consistent with visible tails of enhanced keyboard/CSI-u events:

  • Ctrl+C release: ESC[99;5:3u
  • Ctrl+D press/release: ESC[100;5u / ESC[100;5:3u

Reproducibility

The behavior is intermittent. Repeating the same exit sequence sometimes leaks the CSI-u fragment and sometimes exits cleanly. Quick taps versus holding Ctrl+C/Ctrl+D longer does not appear to affect the occurrence rate.

/quit and /exit have consistently exited cleanly in my testing.

This looks consistent with Codex exiting on the key-press event while the corresponding release event occasionally reaches the parent shell after TUI teardown.

samuel-gamelin · 8 days ago

Follow-up with source-level A/B results and a locally tested candidate fix.

Environment remains macOS arm64 + iTerm2 + zsh. This also appears to be the same failure reported for Kitty in #31865.

What the A/B experiments showed

Baseline codex-cli 0.144.1 intermittently leaked CSI-u tails on control-key exit:

  • Ctrl+C: 9;5:3u
  • Ctrl+D: 00;5u00;5:3u
  • /exit and /quit remained clean

I built current main at 9e552e9d15ba52bed7077d5357f3e18e330f8f38 with only KeyboardEnhancementFlags::REPORT_EVENT_TYPES removed. That did not fix the issue; it changed the leaked packets to:

  • Ctrl+C: 9;5u
  • Ctrl+D: 00;5u

That result suggests the existing exit reset and suppressing release events are both insufficient: a CSI-u packet can remain unread when the app exit path wins the event-loop race.

Source trace / candidate approach

App::run can leave its tokio::select! loop on an app exit event before another terminal event is consumed. The Tui event broker can still own a running crossterm EventStream, while restore_after_exit() resets future keyboard reporting but does not discard unread terminal input.

The locally tested teardown change is:

  1. Pause/drop the crossterm event source immediately after the app loop exits.
  2. Reset keyboard reporting as today.
  3. Flush queued stdin while raw mode/echo suppression is still active.
  4. Restore canonical mode and echo.

The minimal diff is eight added lines in two files:

diff --git a/codex-rs/tui/src/app.rs b/codex-rs/tui/src/app.rs
@@
         };
+        // Stop crossterm's background input reader before final terminal teardown so input queued
+        // behind the exit key cannot be handed to the parent shell.
+        tui.pause_events();
         if let Err(err) = app_server.shutdown().await {

diff --git a/codex-rs/tui/src/tui.rs b/codex-rs/tui/src/tui.rs
@@
     }
     let _ = execute!(stdout(), DisableFocusChange);
+    // On final exit, discard any enhanced-key bytes queued behind the key that ended the app.
+    // Flush after resetting keyboard reporting but before restoring canonical mode and echo.
+    if matches!(keyboard_restore, KeyboardRestore::ResetAfterExit) {
+        flush_terminal_input_buffer();
+    }
     if matches!(raw_mode_restore, RawModeRestore::Disable)

This reuses the existing pause_events() and cross-platform flush_terminal_input_buffer() mechanisms. Temporary restore paths such as external-editor handoff remain unchanged.

Validation

  • Combined pause/flush + no-release-events build: approximately 15–20 clean Ctrl+C/Ctrl+D exits, including exits during MCP startup and normal shell commands immediately afterward.
  • Minimal pause/flush build with upstream REPORT_EVENT_TYPES restored: clean Ctrl+C/Ctrl+D spot checks and normal post-exit shell input so far.
  • just fmt: passed.
  • just test -p codex-tui: 2,988 passed, 4 skipped.
  • Full optimized release builds completed successfully.
  • No observed type-ahead regression. A final-exit tcflush can theoretically discard input deliberately typed before Codex has returned control to the shell, so that is the main tradeoff to evaluate.

The repository's contribution policy asks for analysis/fix outlines in the issue before an external PR. I am happy to prepare the focused patch if a maintainer would like to invite one.

ITJesse · 7 days ago

I investigated this further and have a more targeted candidate patch available:

The source trace agrees that this is an event-loop race: Codex can act on the Ctrl+C/Ctrl+D press and begin teardown before the matching CSI-u release packet is consumed. Resetting keyboard flags only affects future reporting, and a teardown-time stdin flush is timing-dependent: it can run before the release arrives and can also discard legitimate shell type-ahead. In a targeted A/B reproduction of the reported burst, the pause/flush approach still leaked in 4/5 runs.

The candidate patch instead does the following on Unix when enhanced keyboard reporting is active:

  1. When Ctrl+C or Ctrl+D triggers exit, retain that pending exit key.
  2. Pause the crossterm event stream.
  3. Exclusively read stdin one byte at a time until the matching CSI-u release packet is complete (Ctrl+C / codepoint 99 or Ctrl+D / codepoint 100).
  4. Only then allow teardown to continue. One-byte reads stop exactly at the release packet's final u, leaving later shell input queued.
  5. If the exclusive wait times out or errors, restore the pending exit key and crossterm stream so Codex cannot hang permanently. The timeout is 5 seconds.
  6. Pause the event reader again immediately after the app loop exits, before terminal restoration.

Validation on current main:

  • just fmt: passed
  • just test -p codex-tui: 3,047 passed, 4 skipped
  • Ctrl+C release delayed 20 ms: 20/20 clean
  • Ctrl+D release delayed 20 ms: 10/10 clean
  • Ctrl+C delayed 900 ms plus 8 repeats: 5/5 clean
  • Ctrl+D delayed 900 ms plus 5 repeats: 3/3 clean
  • Release delayed 2 seconds: 2/2 clean
  • Release plus TYPEAHEAD in the same PTY write: 10/10 preserved exactly
  • Keyboard enhancement disabled: 5/5 unchanged
  • Release delayed beyond the timeout: Codex still exits after the 5-second fallback

Known boundary: with a synthetic zero-delay release, crossterm can sometimes pre-read the release before the exclusive handoff. All 30/30 runs still exited, but 14/30 used the timeout fallback. When release and type-ahead were sent in the same zero-delay write, type-ahead was preserved in 6/10 runs and had already been consumed by crossterm in 4/10. This seems inherent to handing off from an already-running buffered reader; the normal delayed-release cases were reliable.

I have not reproduced the exact VS Code Remote SSH environment yet; the PTY matrix covered normal, delayed, repeated, held-key, timeout, enhancement-disabled, and type-ahead cases.

The repository currently limits new PRs to collaborators, so I cannot open this against upstream from the fork. I am happy to open the focused PR if invited, or a maintainer can use/cherry-pick the branch above.