Terminal control sequences (SGR mouse reports, etc.) can leak literally into the composer during extended-thinking re-renders
Summary
When running Claude Code interactively inside a terminal that has mouse
tracking enabled (SGR mouse mode, DEC private modes 1000/1002/1003/1006),
scrolling the mouse wheel *while Claude Code is in a long "extended
thinking" / high-effort turn* can cause the raw SGR mouse-report escape
sequence to appear literally in the composer text, instead of being
consumed as a scroll event. The same failure shape (a complete escape
sequence landing in the input buffer as plain text) has also been
observed with other terminal protocol traffic (focus-report sequences,
device/color/query responses), suggesting a more general timing issue
rather than a mouse-specific bug.
Environment
- macOS
- Interactive Claude Code CLI (Ink/React-based TUI), stdin in raw mode
- Terminal multiplexer in between (user's PTY host) with mouse
tracking enabled at the multiplexer layer
- The multiplexer's forwarding of these control sequences to the child
process was independently verified to be correct and complete at the
transport layer (byte-for-byte, not truncated or reordered) — this
points at the CLI's own input handling rather than at anything
upstream of it.
Observed behavior
During a long "thinking" / high-effort turn (visible token-streaming,
frequent re-renders of the composer/status UI), scrolling the mouse
wheel over the pane produces a complete SGR mouse report (including
the leading ESC) inserted literally into the composer text, e.g.
something shaped like:
^[[<65;41;33M
(65 = wheel-down in SGR encoding; the exact row/col varies.) Notably
the sequence arrives whole, with its leading ESC intact — this
is not a case of a sequence being split across two reads and the tail
falling through as stray characters; the entire recognized-as-a-unit
sequence is being treated as literal input.
Expected behavior
Any terminal control/protocol sequence that Claude Code's input layer
is capable of recognizing (SGR mouse reports \x1b[<...M/m, focus
events \x1b[I / \x1b[O, cursor position reports, OSC color-query
responses, device-attribute responses, kitty keyboard protocol
sequences \x1b[...u, etc.) should be intercepted and consumed by a
control-sequence consumer before it can reach the composer's text
reducer, in any rendering/thinking state — not only when the
composer/input UI happens to be in a particular lifecycle phase.
Sequences that are split across two stdin/data read events should
be buffered and reassembled rather than allowed to leak a partial
tail as literal characters after a timeout.
Suspected mechanism (offered as a lead, not a confirmed root cause)
We were not able to instrument Claude Code's own stdin internally, so
this is inference from external, black-box behavior plus general
knowledge of how Ink-based TUIs typically wire up input:
- Ink's core input dispatch (
useInput/ the internal stdindata
listener) does not natively parse mouse SGR reports; Claude Code
must have a separate, purpose-built consumer for mouse (and
possibly other protocol) sequences layered on top of or alongside
Ink's own key-event dispatch.
- The failure only seems to occur while the UI is re-rendering at high
frequency (streaming tokens, spinner/status updates during a long
thinking turn) — i.e. exactly the conditions under which a
React/Ink component tree is most likely to remount or re-attach
effects.
- Our working hypothesis is a mount/drain race between that
control-sequence consumer and the composer's text reducer: if the
consumer is briefly detached (e.g. during a remount of the composer
component, or around a setRawMode toggle triggered by that
remount) at the exact moment a protocol sequence arrives on stdin,
the sequence falls straight through to the text reducer instead of
being intercepted.
- If this hypothesis is right, the fix is less about "handle mouse
sequences specifically" and more about moving control-sequence
filtering to a layer that (a) sits outside/above individual
component lifecycles, so it can't be transiently absent during a
remount, and (b) owns sequence framing so partial/boundary-split
sequences are reassembled rather than timed out and leaked.
- We'd specifically ask the maintainers to check whether
setRawMode
is being toggled (off/on) around composer remounts during long
thinking turns — if so, any protocol bytes that arrive during that
window would predictably be processed in "cooked" line-mode instead
of being intercepted, which would fully explain the observed
leakage.
Steps to reproduce
- Run Claude Code interactively in a terminal (or terminal
multiplexer pane) with mouse tracking enabled (1003h/1006h SGR
mouse mode).
- Trigger a long, high-effort thinking turn that produces visible,
frequent re-renders (streaming output, "still thinking" status
updates).
- While the turn is in progress, scroll the mouse wheel over the
pane (or otherwise cause the terminal to emit protocol responses —
e.g. a focus-in/out event, or a cursor-position/device-attribute/
OSC-color query response reaching stdin at that moment).
- Observe: instead of being consumed as a scroll/protocol event, the
raw escape sequence appears as literal text in the composer once
the turn completes and the composer becomes visible/focused again.
Additional notes
- We believe this same general failure shape (protocol response
bytes ending up as literal composer text) also affects other
terminal-based coding CLIs with a similar input-ownership design; we
are filing a related report against another such CLI separately, in
case the underlying pattern (or fix approach) is useful cross-project
context. We are not claiming the two are the same code path — just
flagging that "a recognized control sequence must never reach the
text buffer, regardless of what internal loading/lifecycle state the
UI is in" seems to be a repeated failure mode in terminal TUI
frameworks and might be worth a shared test matrix.
This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗