Mouse tracking re-enabled from stale altScreenMouseTracking after terminal handoff — 1003 motion floods the composer and breaks arrow keys
Preflight Checklist
- [x] I have searched existing issues and this hasn't been reported yet
- [x] This is a single bug report
- [x] I am using the latest version of Claude Code
What's Wrong?
After Claude Code hands the terminal to a child process, altScreenMouseTracking can be left at "full" while no alt-screen component is mounted. Subsequent mode re-assertions then re-enable any-event mouse tracking (DECSET 1003) with nothing consuming the events.
Every pointer movement over the terminal then emits an SGR report that lands in the composer as literal text. The sustained input rate also severs ordinary key sequences across read boundaries, so arrow keys stop working. A session sitting at an AskUserQuestion prompt becomes completely unusable — neither arrows nor clicks select an option — and has to be killed and --resumed.
The composer of the affected session contained:
BAADD[CC<35;20;18M<35;21;15M…<0;35;41M<0;35;41m…
B A A D D [C C— arrow keys (ESC[B/ESC[A/ESC[D/ESC[C) with theESC[prefix eaten and the tail inserted as text. The inconsistent mangling ([Conce, bareCotherwise) is the signature of read-boundary severing rather than fixed-length framing.<35;…M— SGR reports, button code 35 = 32 (motion bit) + 3 (no button) = any-event motion tracking, i.e. mode 1003.
Root cause
From the 2.1.220 bundle:
ew = { …, MOUSE_NORMAL:1000, MOUSE_BUTTON:1002, MOUSE_ANY:1003, MOUSE_SGR:1006, … }
ec_ = mY(1000)+mY(1002)+mY(1003)+mY(1006) // "full" (enable bundle)
qpe = KSe(1006)+KSe(1003)+KSe(1002)+KSe(1000) // (disable bundle)
vFe(e){ case "full": return ec_; case "scroll": return tc_; case "off": return "" }
handoffAltScreen() { this.isPaused = true; this.altScreenActive = false }
setAltScreenActive(e, t="off"){ if (this.altScreenActive === e) return;
this.altScreenActive = e;
this.altScreenMouseTracking = e ? t : "off"; … }
restoreTerminalAfterHandoff() { this.resumeStdin();
write(vFe(this.altScreenMouseTracking) + SXr); this.resume() }
exitAlternateScreen() { … write(… + vFe(this.altScreenMouseTracking) + …) }
handoffAltScreen()clearsaltScreenActivebut leavesaltScreenMouseTrackingat"full".- When the alt-screen component later unmounts,
setAltScreenActive(false)hitsif (this.altScreenActive === e) return;— it is alreadyfalse— so the tracking mode is never reset to"off". The unmount cleanup itself is correct and does writeqpe, so the terminal is briefly consistent. - The next
restoreTerminalAfterHandoff()/exitAlternateScreen()/onStdinResume → reassertTerminalModeswritesvFe("full"), re-enabling 1000+1002+1003+1006 with nothing mounted to consume the events. - Mode 1003 emits on every pointer movement, so the composer fills with report text and genuine key sequences get severed.
What Should Happen?
Mouse tracking should not be re-enabled when no component is consuming it.
Suggested fix: reset altScreenMouseTracking in handoffAltScreen(), or clear it unconditionally during teardown rather than behind the altScreenActive === e early return.
Steps to Reproduce
- Run Claude Code in a terminal multiplexer pane (observed under
herdr0.8.0 on iTerm2). - Open a full-screen UI that enables mouse tracking and exit it.
/pluginis confirmed to be one — see the measurement below. - Trigger a terminal handoff to a child process.
- Move the mouse over the pane.
Expected: nothing. Actual: <35;…M motion reports accumulate in the composer, and arrow keys begin inserting bare letters.
Confirming /plugin uses mode full. Running script -q log claude, opening /plugin, pressing Esc, then /exit, and counting DEC private mode sequences in Claude Code's own output:
mouse-mode sequences, in order:
1000h 1002h 1003h 1006h × 5 (the "full" enable bundle, ec_)
1006l 1003l 1002l 1000l × 3 (the disable bundle, qpe)
Two things follow. First, /plugin does enable 1003 any-event motion tracking, so it is a valid trigger for the state above. Second, the enable bundle is written 5 times against only 3 disables in a single open → Esc → exit cycle — the pairing is not 1:1, which is consistent with the re-assert path in the root cause. This particular clean run happened to end on a disable, so the terminal was not left poisoned; the poisoning case additionally requires the handoff ordering.
I have not reduced the full failure to a deterministic minimal repro — the handoff ordering is what matters and I observed it only after the fact — but the state machine above accounts for every byte observed, and the enable/disable imbalance is reproducible on demand.
Relationship to existing issues
- #78855 (input parser types the tail of SGR mouse reports split across starved reads) is the downstream half of this. That report needs host load or a synthetic 200-report burst to fire, and its author notes they could not reproduce at human wheel rates (~10–30 reports/s). Leftover 1003 motion tracking supplies exactly that rate from ordinary pointer movement, which makes the parser fragility fire constantly rather than intermittently.
- #77752 (mouse tracking not disabled on Ctrl-Z suspend) and #72648 (modes left enabled after session exit) are the same pause/teardown family, but they leak outward to the shell. This one re-enables tracking inward and poisons Claude Code's own input.
- #78306 (AskUserQuestion unresponsive to mouse and keyboard input) looked like a duplicate from its title, but on reading it appears unrelated — it reports a picker that accepts no input, with no escape-sequence text in the composer, in a two-column preview layout. That looks closer to #81755.
Ruled out: not a multiplexer bug
A raw-stdin dumper requesting exactly ESC[?1000h + ESC[?1006h was run in a fresh multiplexer pane while the pointer was moved, scrolled and clicked over it:
| Test | Pane focused | Reports | Codes seen | Motion (32+) |
|------|--------------|---------|------------|--------------|
| 1 | no | 318 | wheel-up 198, wheel-down 80, click 40 | 0 |
| 2 | yes | 464 | wheel-up 298, wheel-down 154, click 12 | 0 |
The multiplexer delivers exactly the mode set the pane requests, focused or not, and never synthesizes motion events.
Workarounds
CLAUDE_CODE_DISABLE_MOUSE_CLICKS=1→ selects"scroll"(1000+1006, no motion): keeps wheel scrolling and removes the flood.CLAUDE_CODE_DISABLE_MOUSE=1→ selects"off": no mouse modes emitted at all.- Writing
ESC[?1003l ESC[?1002l ESC[?1000ldirectly to the pane's tty does not hold — the app re-assertsvFe("full")within minutes. Verified: motion reports in the composer went from 26 to 42 after the reset, with new wheel reports appearing. - Reliable recovery once a pane is poisoned is to close it and
claude --resume.
Claude Code Version
2.1.220 (Claude Code), Homebrew cask
Platform
Claude subscription
Operating System
macOS (Darwin 25.6.0)
Terminal/Shell
iTerm2 + zsh, inside the herdr 0.8.0 terminal multiplexer
This issue has 2 comments on GitHub. Read the full discussion on GitHub ↗