[BUG] Crash leaves the terminal in mouse-tracking mode — the restore handler is registered on graceful exit, so it can never fire on the path that needs it

Status Open
Reported on v2.1.220
Maintainer reply ✓ Yes — bcherny
Activity 3 comments · opened Aug 5, 2026
💡 Likely answer: A maintainer (bcherny, collaborator) responded on this thread — see the highlighted reply below.

Summary

The TUI enables mouse-tracking mode at startup. When a session crashes, the corresponding disable sequences are never emitted, leaving the terminal in a mode the user never selected. Every subsequent mouse movement injects raw escape sequences into the shell prompt.

What it looks like

After two sessions crashed, the shell began emitting this on every pointer movement:

PS …> [555;35;27M[555;38;27M[555;43;28M[555;49;28M[555;57;30M[555;68;32M…

Those are SGR mouse-reporting events. The climbing coordinate pairs — 35;27 → 38;27 → 43;28 → 49;28 → 57;30 — are literally the mouse moving across the window, each motion writing a control sequence onto the command line.

The affected tabs also lost their session titles and reverted to a generic shell name, which was the only visual indication anything had died.

Root cause

This is not a missing cleanup routine — the cleanup exists and emits exactly the right sequences (?1000l, ?1002l, ?1003l, ?1006l, ?1049l — press-only, button-event, any-event, SGR extension, alternate screen).

The problem is where it is registered. It runs on the graceful-exit lifecycle event, and additionally returns early unless stderr is a TTY.

A crash emits no lifecycle event. So the terminal-restore logic is hosted inside the process whose death is the failure it exists to remediate.

It covers the path that never needed covering — orderly shutdown — and is structurally incapable of covering the only path that does.

Recovery requires knowledge no user should need

Clearing it by hand means knowing that mouse-tracking modes exist, that a crash leaves them set, and what their disable codes are:

Write-Host "$([char]27)[?1000l$([char]27)[?1002l$([char]27)[?1003l$([char]27)[?1006l"

Most users will close the tab instead — losing scrollback and any in-flight context — or assume the terminal itself is broken.

Suggested fix

Terminal modes are process-global state the TUI mutated, and process death is a supported outcome, so restoration cannot live inside the process. Either:

  1. Restore from the launcher/parent on child exit, regardless of exit reason (still defeated by a hard kill, but covers crashes), or
  2. Emit a defensive reset on shell-prompt redraw — the only mechanism guaranteed to run after any abnormal exit, since the shell always redraws.

Additionally, dropping the stderr.isatty() early-return would let the reset fire in more of the cases where it matters.

Environment

  • Claude Code 2.1.220 (recurring; also seen on earlier 2.1.x)
  • Windows 11, Windows Terminal
  • Reproduces when a session dies abnormally rather than exiting cleanly
  • Screenshot available on request

View original on GitHub ↗

3 Comments

ThatDragonOverThere · 22 days ago

Recurring, and it now has a reliable upstream cause: the classifier false-positive in #83436

Fresh occurrence today. The terminal fills with raw SGR mouse-report sequences after the session dies — the shell is left echoing coordinate data on every pointer movement:

[555;204;39M[555;203;39M[555;203;38M[555;202;38M[555;201;38M ...

Unusable until the terminal is manually reset, exactly as described in the original report.

The part that's new, and I think it matters for prioritization

This bug's severity is a function of how often the ungraceful exit path is taken. When it was filed, that path was assumed rare — real crashes.

It isn't rare anymore. Over roughly 24 hours I've recorded seven session-ending API errors from the classifier false-positive documented in #83436 (also reported in #82440, #82139, #70822). Every one of those is an abnormal termination — the exact path where, per this issue, the restore handler cannot fire because it was only ever registered on graceful exit.

So the two defects compound directly:

  • #83436 kills sessions unpredictably, on ordinary work — one instance died performing a single Read on a local JSON file.
  • #84029 (this issue) means every one of those deaths leaves the terminal broken and requires manual recovery.

The result is that a user hitting the classifier bug doesn't just lose the session — they lose the terminal too, and have to reset it before they can even start the replacement session they were told to start. The error message literally instructs "Start a new session to continue," while the exit path it took makes the terminal unusable for doing so.

Why this is worth fixing regardless of #83436

Even if the classifier issue is resolved tomorrow, this is a one-line-class defect with a disproportionate blast radius: a restore handler registered only on the graceful path can never run on the path that needs it. Any abnormal termination — crash, kill, network drop, API error — leaves the terminal in a broken state.

Registering the terminal-restore on abnormal-exit paths as well (signal handlers, uncaught-exception handler, and the API-error termination path specifically) would make every one of these failures recoverable instead of requiring a manual reset.

This has been open since 2026-08-05 with no comments. Given it now amplifies a defect firing multiple times a day, it seems worth more than a backlog slot.

wador-ai · 16 days ago

Adding a data point for another abnormal-termination path with the same root cause: abrupt SSH disconnect while the TUI is running remotely.

Environment

  • Client: Windows 11, Windows Terminal, OpenSSH client (PowerShell profile)
  • Server: Ubuntu Server 24.04, Claude Code 2.1.233 launched directly in the SSH session (no multiplexer)

Symptom

When the SSH session drops while the TUI is active, the local terminal tab starts flooding on every mouse movement:

41M35;37;41M35;38;41M35;39;41M35;40;41M35;41;...

These are SGR mouse reports (?1006) with the ESC [ < prefix stripped by the echo; the coordinates track the pointer continuously. The shell stays usable but unreadable until a blind reset / manual ?100xl sequence, or closing the tab.

Repro

  1. ssh <server>, run claude
  2. Kill the link (drop the network, or server-side pkill -f 'sshd.*<user>')
  3. Move the mouse over the terminal window

Why this path matters for the proposed fixes

For the SSH case, even a SIGHUP/SIGTERM handler in the process would not be sufficient: the restore sequences have to travel over the SSH link to reach the client-side emulator, and by the time the process gets SIGHUP the link is already gone — the ?100xl codes have nowhere to go. So of the two options in the issue description, option 1 (restore from the process/parent on exit) is structurally unable to cover the network path; only something client-side (option 2, defensive reset on prompt redraw) can.

Workarounds confirmed

  • Recovery: printf '\e[?1000l\e[?1002l\e[?1003l\e[?1006l\e[?1015l\e[?25h' + stty sane (or PowerShell Write-Host equivalent client-side)
  • Prevention: run claude inside tmux on the server — the TUI survives the disconnect and sshd's own teardown leaves the client terminal clean. A tmux new-session -A -D -s claude wrapper has been working reliably for us.
bcherny collaborator · 14 days ago

Thanks for the detailed report. I tested this on 2.1.233 (macOS, tmux, so I could read the terminal's mode flags directly after the process died):

  • External SIGINT, SIGTERM, and SIGHUP: mouse tracking and the alternate screen were restored every time (mode flags went back to 0). So the restore is not only wired to the graceful /exit path — signals and the internal uncaught-exception shutdown paths run the same terminal reset.
  • Hard kill (kill -9) or a native crash of the runtime (kill -SEGV, which prints the "Bun has crashed" panic): the terminal was left in mouse-tracking mode and on the alternate screen — reproduced. Nothing inside the process can run at that point, so no in-process handler can cover this, same as any full-screen terminal program.
  • The "can't help with this. Start a new session to continue" message mentioned in the comments does not terminate the process — the session stays open and exits cleanly later — so it should not be leaving the terminal in this state on its own.
  • SSH link drop: as you note, even a perfect handler can't reach the client emulator once the link is gone; running under tmux on the server (or reset / the ?100xl sequences locally) is the right workaround.

So the remaining gap is native crashes and hard kills. We agree the outcome is bad and are considering options (a supervisor/launcher-side reset on abnormal child exit is the most plausible; a defensive reset on shell prompt redraw would be a shell-config change rather than something Claude Code can ship). If you hit this on Windows and have any crash output from the terminal or claude doctor for the run that died, please add it — that would tell us which crash to chase.

🤖 Generated with Claude Code