Root cause + fix for scroll jumping on long responses (CSI 2J inside DEC 2026 sync blocks)

Resolved 💬 3 comments Opened Mar 18, 2026 by inchwormz Closed Mar 21, 2026

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:

  1. Parse output for DEC 2026 sync block boundaries (BSU/ESU)
  2. Check if the sync block contains both CSI 2J (erase display) and CSI H (cursor home). That's the full-redraw signature.
  3. If yes: strip CSI 2J entirely, strip CSI H only at position 0 (not mid-content), re-wrap in BSU/ESU
  4. 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:

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

View original on GitHub ↗

This issue has 3 comments on GitHub. Read the full discussion on GitHub ↗