Ctrl+G external editor ($EDITOR) leaks mouse/keyboard escape sequences into chat input
Summary
When Ctrl+G opens the external editor, Claude Code:
- does not temporarily disable its mouse tracking (
CSI ?1006h), any-event mouse (?1003h), button-event mouse (?1002h), normal mouse (?1000h), or focus tracking (?1004h); - does not flush the kernel tty input queue when the editor returns.
As a result, any keystrokes, mouse movements, clicks, focus changes, or wheel scrolls the user performs in the terminal while the editor is open are queued on the shared tty and are then read as user input by Claude Code after the editor exits. They land literally in the chat composer.
Concrete Reproduction
export EDITOR="code --wait"(or any GUI editor that waits without reading stdin, e.g.EDITOR="sh -c 'sleep 30'").- Start
claudein a mouse-aware terminal emulator (WezTerm, iTerm2, kitty, etc). - Type a few characters into the chat composer, then press Ctrl+G.
- While VS Code is open:
- move the mouse inside the Claude Code pane,
- click a few times,
- spin the mouse wheel,
- press arrow keys,
PgUp/PgDn,Home/End, - alt-tab so focus tracking fires.
- Save and close VS Code.
- Observe the chat composer — it now contains runs like:
````
^[[O^[[I^[[<0;93;60M^[[<32;93;60M^[[<0;93;60m
^[[<35;92;60M^[[<35;90;60M... (SGR mouse events)
^[[A^[[A^[[B^[[B^[[B (arrow keys from wheel-to-arrow translation)
^[[5~^[[6~^[[H^[[F (PgUp/PgDn/Home/End)
Root Cause
Claude Code leaves terminal tracking modes asserted across the $EDITOR invocation:
CSI ?1000h/?1002h/?1003h/?1006h— mouse reportingCSI ?1004h— focus reporting
While these are on, the terminal emulator keeps writing escape sequences into the shared tty. And because code --wait (and other GUI editors) never read from the tty, those bytes accumulate in the kernel input queue. When Claude Code resumes, it reads the queue as user input.
Separately, even with mouse tracking disabled, WezTerm's default wheel binding translates WheelUp/WheelDown into CSI A / CSI B (arrow keys) for pager compatibility. These also queue up.
Workaround Attempts and Their Limits
I implemented a shell wrapper ($EDITOR=/path/to/claude-vscode-edit) that:
- Writes
CSI ?1000l ?1002l ?1003l ?1006l ?1004lto/dev/ttybefore launchingcode --wait. - Calls
termios.tcflush(fd, TCIFLUSH)on/dev/ttybefore and again after the editor exits (after a 300 ms grace period + a second flush, to catch in-flight events from the editor close). - Re-enables the tracking modes on return.
Debug log from a real session:
[01:23:42.529] === invoke pid=159644 ===
[01:23:42.535] before mouse disable
[01:23:42.536] mouse/focus disable written
[01:23:42.561] tcflush(pre-editor): ok
[01:23:42.563] spawning: code --wait /tmp/claude-prompt-....md
[01:24:12.629] code --wait exit rc=0
[01:24:12.969] tcflush(post-editor-1): ok
[01:24:13.096] tcflush(post-editor-2): ok
[01:24:13.097] mouse re-enable
[01:24:13.099] === exit 0 ===
All tcflush calls return success. Even so, arrow-key sequences still leak into the chat composer. The reason:
There is a race window between the wrapper exiting (01:24:13.099) and Claude Code resuming its stdin read loop and re-asserting its own terminal modes. During that window, wheel events (or any keystroke) still land in the tty queue and are read by Claude Code as user input. This window is in the tens-to-hundreds of milliseconds and cannot be closed from user space.
Therefore the wrapper mitigates mouse-event leakage but cannot eliminate keystroke/wheel leakage without support from Claude Code itself.
Requested Fix
When invoking $EDITOR, Claude Code should:
- Before
fork/exec:
- Emit
CSI ?1006l ?1004l ?1003l ?1002l ?1000l(disable the tracking modes it turned on).
- On return, before resuming the input loop:
- Call
tcflush(STDIN_FILENO, TCIFLUSH)(or equivalent) to drop any bytes that queued up while the editor held the tty. - Re-assert whichever tracking modes the app uses.
This is the same convention vim, less, htop and other curses TUIs use when suspending into a shell or invoking a sub-process.
Alternatively or additionally, provide a documented opt-out such as a --no-mouse flag / CLAUDE_CODE_NO_MOUSE=1 env var / "mouseTracking": false in settings.json. Related: #23581.
Related Issues
- #10375 — focus reporting sequences leak into input (still open)
- #23581 — feature request for
--no-mouseflag (still open) - #27001 — Ctrl+G with kitty keyboard protocol
- #9218 — Ctrl+G terminal corruption
- #17787 — DSR 6 leak on macOS
The keyboard/wheel-sequence leakage with GUI editors is, to my knowledge, not covered by any of these.
References
- xterm control sequences (SGR mouse, focus tracking, DEC private modes 1000/1002/1003/1004/1006)
- POSIX
tcflush(3) - WezTerm default mouse bindings (wheel → arrow-key translation when mouse tracking is off)
This issue has 3 comments on GitHub. Read the full discussion on GitHub ↗