Root cause + fix for scroll jumping on long responses (CSI 2J inside DEC 2026 sync blocks)
Problem
When Claude Code streams long responses (400+ lines), the terminal scroll position resets to the top of the visible output on every render update. You can't read earlier output while new content arrives. This is the most upvoted UX issue across AI CLI tools (see #1208, #656, #548).
Root cause
Claude Code uses DEC 2026 synchronized output (BSU/ESU markers) to batch screen updates atomically. Good.
The problem: when a response exceeds the visible terminal height, Claude Code emits a full-screen redraw inside the sync block:
BSU (begin sync)
CSI H ← cursor home (move to 0,0)
CSI 2J ← erase entire display
[full content re-render]
ESU (end sync)
CSI 2J (erase display) resets the terminal's scroll position. The terminal processes the entire sync block atomically, so the scroll position snaps to the top after every update.
Short responses use incremental updates and don't include CSI 2J. They scroll fine. The bug only fires on full redraws.
Fix
Strip CSI 2J and the leading CSI H from sync blocks that contain a full-screen redraw. Content still renders correctly because cursor positioning sequences within the content handle placement. The BSU/ESU wrapper stays intact, so the terminal still processes the update atomically.
Detection logic:
- Parse output for DEC 2026 sync block boundaries (BSU/ESU)
- Check if the sync block contains both
CSI 2J(erase display) andCSI H(cursor home). That's the full-redraw signature. - If yes: strip
CSI 2Jentirely, stripCSI Honly at position 0 (not mid-content), re-wrap in BSU/ESU - If no: pass through unmodified, preserving original timing
Important: normal sync blocks (incremental updates) must pass through untouched. Intercepting all sync blocks breaks timing and causes a different scroll regression.
Working implementation
FurbySoup/quell is an open-source Windows ConPTY proxy that implements this fix. Tested against Claude Code:
- 398-line single response: no scroll jumping
- 5 back-to-back responses (100-200 lines each): scroll position held while scrolling between responses
- Short, medium, long, code-heavy, mixed content: all pass
Relevant code:
src/vt/sync_detector.rs: DEC 2026 sync block detection, full-redraw identificationsrc/proxy/mod.rs:strip_clear_screen()function, two-pass proxy loop
The fix is ~50 lines of logic. The same approach works natively in Claude Code's rendering pipeline, removing the need for an external proxy.
Suggested integration path
Option A: stop emitting CSI 2J during streaming updates. Use cursor positioning (CSI <row>;<col>H) and selective line clearing (CSI 2K) to update only changed regions. This is how htop and vim avoid scroll jumping during full-screen updates.
Option B: if full redraws are architecturally required, strip the CSI 2J before writing to stdout when inside a sync block. Content renders correctly without it.
Environment
- Windows 11, Windows Terminal 1.25+
- Claude Code latest
- Also reproduced on macOS Terminal and iTerm2 (same VT sequence pattern)
References
- #1208 (scroll jumping megathread)
- FurbySoup/quell (MIT licensed)
- DEC 2026 spec
This issue has 3 comments on GitHub. Read the full discussion on GitHub ↗