[BUG] Fullscreen TUI: bare mouse movement scrolls the viewport after any wheel notch — SGR reports with both the wheel and motion bits set are treated as wheel notches (urxvt)

Status Open
Reported on v2.1.220
Maintainer reply None cached
Activity 1 comment · opened Aug 3, 2026
Disclaimer: this report was written and filed by Claude Code (Opus 5) using the account holder's gh credentials, with their explicit review and approval. The diagnosis, the byte captures, and the local urxvt patch referenced below were all produced during that session.

TL;DR

In the fullscreen renderer, any bare mouse movement scrolls the conversation once the user has scrolled the wheel even a single notch. The proximate cause is a terminal bug (urxvt 9.31 keeps the wheel button latched and sets both the wheel bit and the motion bit in its SGR reports), but Claude Code responds to ESC [ < 96 ; x ; y M as though it were a wheel-up notch and scrolls.

Suggested hardening: when parsing an SGR mouse report, test the motion bit (32) before the wheel bit (64). A wheel cannot drag, so a report with bit 5 set is a motion event no matter what bit 6 says.

To be clear about what I did and did not verify: I have measured the bytes going in and the scrolling coming out, and that causal link is solid. I have not inspected Claude Code's parser — the wheel-bit-takes-precedence explanation is inferred from the behaviour, so treat it as a hypothesis about the implementation rather than a reading of it.

Environment

| | |
|---|---|
| Claude Code | 2.1.220 |
| Renderer | fullscreen ("tui": "fullscreen") |
| Terminal | rxvt-unicode (urxvt) 9.31 |
| OS | Arch Linux, X11 |
| tmux | reproduces both inside tmux 3.7b and in a plain urxvt window with no tmux |

Reproduction

  1. Run Claude Code with the fullscreen renderer in urxvt.
  2. Scroll the wheel one notch.
  3. Move the mouse without touching the wheel.

The viewport keeps scrolling, one notch per pointer movement, indefinitely. Clicking does not clear it. CLAUDE_CODE_DISABLE_MOUSE_CLICKS=1 suppresses it but also disables drag-select, so in-app copy stops working entirely.

<details>
<summary>Byte-level capture of what the terminal actually sends (DECSET 1000+1006+1003, exactly what the fullscreen renderer enables)</summary>

Captured with a small probe that enables the same modes Claude Code does and decodes each report. raw_b is the SGR button field.

# bare pointer motion, wheel untouched
[   1] SGR x=19 y=6  btn=none      flags=-                    raw_b=31   b'\x1b[<31;19;6M'
[   4] SGR x=25 y=6  btn=none      flags=-                    raw_b=31   b'\x1b[<31;25;6M'

# one wheel-up notch -- correct
[   5] SGR x=25 y=6  btn=WHEEL-UP  flags=wheel(64)            raw_b=64   b'\x1b[<64;25;6M'

# bare pointer motion again, wheel still untouched -- WRONG
[   6] SGR x=27 y=7  btn=WHEEL-UP  flags=wheel(64)+motion(32) raw_b=96   b'\x1b[<96;27;7M'
[   7] SGR x=29 y=7  btn=WHEEL-UP  flags=wheel(64)+motion(32) raw_b=96   b'\x1b[<96;29;7M'
[   8] SGR x=30 y=7  btn=WHEEL-UP  flags=wheel(64)+motion(32) raw_b=96   b'\x1b[<96;30;7M'

96 = 64 | 32. If the parser checks the wheel bit first, 96 & 3 == 0 reads as WHEEL-UP and each movement scrolls. If it checks the motion bit first, it is correctly a motion event with no button held.

The 31 on the pre-wheel lines is a second urxvt bug (0 - 1 + 32, no button ever pressed) that upstream has already fixed; xterm sends 35 there.

</details>

<details>
<summary>Root cause on the terminal side, and its upstream status</summary>

urxvt's mouse_report() computes button_number = MEvent.button - Button1 and adds 64 - 3 for the wheel range. button_release() returns early for the wheel "buttons" so their release is not reported — but it returns before MEvent.button is reset, so the wheel button stays latched forever. Every later motion notify event then encodes the wheel button, with the motion bit ORed in.

Upstream commit 5ec6fb5e ("Fix mouse reporting of motion notify events") fixes the never-clicked -1 case and adds the reset for buttons 1–3, but the wheel path still returns before reaching it. There is no upstream fix for the wheel latch, and 9.31 (2023-01-02) is the newest release — the project is effectively dormant. I have patched it locally and verified the fix, but a stock urxvt will keep doing this.

</details>

<details>
<summary>Why hardening Claude Code is still worth it</summary>

  • urxvt is unmaintained, so every stock urxvt will keep emitting these reports.
  • The combination wheel+motion is meaningless in the protocol, so ignoring the wheel bit when the motion bit is set cannot regress a correct terminal.
  • vim hit the same class of bug with urxvt motion reports colliding with wheel codes (vim/vim#9863). That was also urxvt, so it is not evidence of other terminals doing this — but it does show the report shape trips up more than one application, and that each one has had to decide the bit precedence for itself.
  • The only current workarounds cost real functionality: CLAUDE_CODE_DISABLE_MOUSE_CLICKS=1 loses drag-select and copy, and /tui default loses the fullscreen renderer.

</details>

View original on GitHub ↗

This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗