Renderer corrupts screen in long iTerm2 sessions — CLI emits cursor-up sequences far larger than the viewport, repaint anchors to row 1 (regression after 2.1.162, still present in 2.1.177)

Status Open
Reported on v2.1.140
Maintainer reply None cached
Activity 7 comments · opened Jun 14, 2026

Summary

In long-running / resumed sessions on macOS iTerm2, the TUI renderer progressively corrupts the screen: the prompt jumps to the top of the viewport and stale content below is slowly overwritten on subsequent repaints. A full redraw (Ctrl+L) recovers temporarily, then it recurs. Short / fresh sessions are unaffected.

Root cause is captured below from a raw script tty log: the CLI itself emits cursor-up escape sequences far larger than the terminal height (up to ESC[712A), which clamp at the top of the screen and cause the next repaint to start at row 1. The capture is forensically dispositive and I can attach it.

Environment

  • Claude Code versions:
  • Clean: 2.1.140 (long historical use), 2.1.162 (real-world use, no corruption observed over a full working day).
  • Corrupting: 2.1.170 and 2.1.173 (forensic capture below), and 2.1.177 (a live session running 2.1.177 reproduced the corruption; not separately captured).
  • 2.1.169: ambiguous, not conclusively classified.
  • So the regression was introduced somewhere in 2.1.163–2.1.170 and persists through the latest build (2.1.177). The exact introducing build is not yet bisected.
  • OS: macOS (Darwin 25.4.0)
  • Terminal: iTerm2 3.6.9
  • Renderer: default TUI renderer (tui: default in settings, which uses the terminal's alternate screen buffer)
  • TERM=xterm-256color, no tmux
  • Aggravators present in this setup (not confirmed as causes): long resumed sessions, a custom statusline, and the remote-control footer (a persistent footer line in the TUI)

Root cause (from raw tty capture)

I captured the renderer output with script while corruption was occurring. The CLI emits cursor-up escape sequences far larger than the terminal can hold:

  • ESC[<N>A with N ranging from 101 up to 712.
  • 3918 such oversized moves in a single capture, out of 14843 cursor-up moves total.
  • For reference, the largest cursor-up observed in a clean (non-corrupting) session is 59 rows. Any move past the terminal height is illegitimate: the terminal clamps the cursor at row 1 (it cannot scroll above the top of the screen), so the subsequent repaint begins at row 1 and overwrites on-screen content.
  • Also observed: width desync — the renderer paints at a stale, narrower width (text breaks one word per line), suggesting the renderer's internal column/row model has drifted from the terminal's actual size.

This is consistent with the renderer tracking a growing assistant response as its repaint frame, computing a cursor-up larger than the physical screen, and anchoring the repaint to the top instead of the correct row.

Anyone can reproduce the analysis on the attached capture with:

perl -ne 'while(/\e\[(\d+)A/g){$t++; $b++ if $1>=100} END{printf "%d cursor-up total, %d exceed 100 rows\n",$t,$b}' capture.log

Steps to reproduce

  1. Resume a long session in iTerm2 on 2.1.170+ (default TUI renderer).
  2. Use it through several streamed responses. A monitor resize / display change while a response is rendering appears to trigger onset.
  3. The screen begins jumping to the top with stale text overwritten below.

Reproduction caveat: a fresh session reproduces poorly. We were unable to reproduce with any zero-token synthetic trigger on a known-bad build — idle resize storms, paste storms, transcript-viewer scrolling, and bash-mode local streaming, each combined with window resizes, in both detached ptys and a real iTerm2 window. All stayed clean (max cursor-up 59, i.e. correct). Reproduction appears to require real model streaming (a growing assistant message) plus accumulated long-session renderer state.

Not terminal-specific

This was only observed in iTerm2, but the captured sequences (ESC[712A) would corrupt any terminal that clamps cursor-up at row 1, which is standard VT behavior. The fault is in the emitted sequences, not in iTerm2's handling. Other terminals (Terminal.app, kitty) were not tested.

Workarounds tried

  • CLAUDE_CODE_FORCE_SYNC_OUTPUT=1 — insufficient, recurs.
  • Remote control off — insufficient, recurs.
  • Pin to a pre-regression build (2.1.140, or 2.1.162) — resolves it.

Impact

Sessions become unusable without repeated Ctrl+L. The only reliable fix is to downgrade and pin to an old build, which blocks adoption of current releases.

I have the raw script capture with the corruption in the act (≈1.9 MB) and can attach it on request.

View original on GitHub ↗

7 Comments

yurukusa · 2 months ago

Excellent capture — the oversized ESC[<N>A (up to 712) plus the width desync is the clearest articulation of this I've seen. The two observations together (cursor-up larger than the viewport and a stale narrower paint width) point at the same thing: the renderer's internal row/column model has drifted from the terminal's actual size, so it computes a repaint frame taller and narrower than the screen and anchors to row 1.

Two practical notes for while this is open:

  • A renderer-mode toggle tends to reset the drifted state more durably than Ctrl+L. Ctrl+L forces a redraw but the renderer keeps its stale row/column model, so it recurs (matching what you saw). Flipping the render mode and back — e.g. change tui to the other setting and return to default (or toggle it live if your build exposes that) — reinitializes that internal model from the current terminal size. Worth confirming on your repro: does a mode round-trip hold longer than Ctrl+L before recurrence? If yes, that's another data point that the bug is purely in the cached size model, not the draw path.
  • Your trigger note is the actionable mitigation: onset needs (a) accumulated long-session renderer state and (b) a resize/display-change while a response is streaming. So until it's fixed: avoid monitor/resize changes mid-stream, and /clear or a fresh session periodically — you already noted fresh sessions stay clean (max cursor-up 59), which lines up with the state being session-accumulated rather than version-constant.

This also rhymes with the broader 2.1.x renderer-state-drift reports (mouse-wheel-emitting-arrows, tmux scrollback breakage) where the common thread is the renderer mis-tracking terminal geometry after a mode/size transition. The fact that yours reproduces only with real model streaming (a growing assistant message) is the sharpest pin yet on where the row count is being miscomputed — that's the detail that should make this bisectable. The script capture would be very much worth attaching.

github-actions[bot] · 2 months ago

Found 3 possible duplicate issues:

  1. https://github.com/anthropics/claude-code/issues/67115
  2. https://github.com/anthropics/claude-code/issues/66811
  3. https://github.com/anthropics/claude-code/issues/66795

This issue will be automatically closed as a duplicate in 3 days.

  • If your issue is a duplicate, please close it and 👍 the existing issue instead
  • To prevent auto-closure, add a comment or 👎 this comment

🤖 Generated with Claude Code

brendanx67 · 2 months ago

Bisected the exact introducing build on Windows (the OP brackets it at 2.1.163–2.1.170): first-bad = 2.1.163, last-good = 2.1.162 (adjacent; 2.1.164 was never published to npm, 2.1.165→2.1.177 all bad).

Likely culprit: the 2.1.163 changelog entry "Fixed native terminal cursor not staying at the input caret when the terminal loses focus." That change moved cursor handling onto the real native terminal cursor — consistent with the oversized ESC[<N>A emissions captured here.

Human-visible tell that matches the byte-level finding: 2.1.162 draws a synthetic block cursor in the prompt that stays anchored; 2.1.163+ uses the real blinking cursor, which drifts up-screen with output before corruption sets in. So a build can be classified good/bad at session start, no long session needed.

Also reproduces on Windows Terminal + PowerShell 7.6.2 across three machines (Win10 22H2, Win11 23H2, Win11 25H2) — supporting the OP's "not terminal-specific" call.

reed-sandberg · 1 month ago

Something has changed with compatibility between iterm2 and claude wrt iterm2 scrollback and claude's TUI drawing/repainting/backfilling within the last 3 months or so. Used to work perfectly a few months ago but now the session history is often mangled with lines missing/overwritten in claude output and responses. Sounds related to this issue rendering iterm2 (one of the most popular mac terminal applications globally) useless because I'm unable to read claude's answers!

Works fine with ghostty, so am having to ditch iterm2 entirely when using claude code (which is daily use at this point).

UPDATE - the following also eliminates the scrollback corruption:
export TERM=vt100

jontsai · 1 month ago

Additional data point: still present in 2.1.210, and it also reproduces inside tmux — with the opposite renderer mode to the OP.

Environment

  • Claude Code 2.1.210 (latest at time of writing)
  • macOS (Darwin 25.5.0), Apple Silicon (M5)
  • iTerm2 3.6.11 → tmux 3.7a (plain tmux attach, not -CC), TERM=tmux-256color inside panes
  • Renderer: tui: fullscreen (alternate screen buffer; pane confirmed alternate_on=1)
  • tmux config verified healthy for this: allow-passthrough on, and sync/extkeys present in the attached client's negotiated client_termfeatures. Single client per session at the correct size (no multi-client resize contention).

Repro

In a fresh session ~5 minutes old (so no accumulated long-session state), typed /resume. The picker painted only its footer line ("search · Esc to cancel"); the body of the screen kept the stale welcome banner. Verified from outside via tmux capture-pane -p: tmux's grid itself held the stale mix, i.e. the CLI never emitted the picker body — not a tmux→terminal desync.

Recovery behavior (may help localize the fault)

  • Ctrl+L → no effect (body still stale).
  • Forced SIGWINCH (tmux resize-window ±1 column and back) → no effect. The renderer appears convinced the content is already on screen, so it repaints nothing — consistent with the drifted internal frame model described in the OP.
  • Esc out of the picker, then switching renderer mode (/tui default) → immediate full clean repaint, and the default renderer then painted /resume correctly on the first try inside tmux.

Notes

  • Interesting inversion vs. the OP: OP sees corruption with the default renderer in bare iTerm2 (fullscreen as workaround); here the fullscreen renderer corrupts inside tmux almost immediately (default as workaround). Both point at the same drifted row/column model, just triggered in different renderer/terminal combinations.
  • Inside tmux the fullscreen corruption does not need long sessions or model streaming — a fresh session + the /resume overlay is enough.

This should not be auto-closed as a duplicate; it is the canonical capture-backed report for this regression.

jontsai · 1 month ago

Follow-up to my earlier tmux data point, after a day of systematic testing in the same environment (macOS Darwin 25.5.0, iTerm2 3.6.11 → tmux 3.7a, plain attach): on 2.1.210, all three renderer modes are broken inside tmux, and downgrading to 2.1.162 fixes all of them — a clean user-side confirmation of @brendanx67's 2.1.162→2.1.163 bisection.

2.1.210 renderer matrix (inside tmux)

| Renderer | Result |
|---|---|
| tui: fullscreen | Corrupts within minutes, even in a fresh ~5-min session: /resume picker painted only its footer line, body kept stale content. Ctrl+L and forced SIGWINCH do not recover; toggling renderer mode does. |
| tui: default | Screen stops refreshing during normal use. |
| Classic (CLAUDE_CODE_DISABLE_ALTERNATE_SCREEN=1) | Streaming survives (even with 4 mid-stream window resizes), but interactive overlays corrupt: the /resume picker painted scattered single-line fragments mixed with rows from the previous frame (slash-command autocomplete), with most rows left unpainted. Verified via tmux capture-pane — the corruption is in the pane grid, i.e. in the CLI's emitted sequences, not a tmux→terminal desync. |

2.1.162, same environment, same tests

Everything clean: /resume picker fully painted, arrow-key navigation repaints per keypress, resize while the picker is open triggers a complete correct redraw, and long streaming turns paint contiguously.

Workaround note for affected users

Pinning 2.1.162 takes more than installing it: the auto-updater re-pointed ~/.local/bin/claude back to 2.1.210 twice within minutes of claude install 2.1.162 (any running newer instance appears able to "repair" the symlink). Pinning required DISABLE_AUTOUPDATER=1 plus locking the symlink (chflags -h uchg on macOS). A supported way to temporarily hold a version would make this regression much easier to live with while the fix lands.

lawrencecchen · 1 month ago

I investigated the corresponding cmux reports and found direct evidence supporting the 2.1.162 -> 2.1.163 bisection.

I compared the public @anthropic-ai/claude-code-darwin-arm64 2.1.162 and 2.1.163 binaries. The native-cursor decision changed in the first-bad build:

2.1.162: accessibility -> on; explicit CLAUDE_CODE_NATIVE_CURSOR -> terminal-dependent; otherwise off
2.1.163: accessibility -> on; explicit CLAUDE_CODE_NATIVE_CURSOR -> terminal-dependent; excluded renderer -> off; otherwise featureFlag("tengu_native_cursor", false)

So 2.1.163 introduced a remotely controlled native-cursor rollout on the exact adjacent first-bad build. CLAUDE_CODE_NATIVE_CURSOR=0 does not force-disable it, because a false value falls through to tengu_native_cursor. This aligns with the real cursor replacing the synthetic cursor, cursor/input drift, and Vim mode being a strong trigger in https://github.com/anthropics/claude-code/issues/67115.

I also captured Claude 2.1.211 directly from a PTY. CLAUDE_CODE_ALT_SCREEN_FULL_REPAINT=1 had no structural effect in default mode, while fullscreen erase-line operations increased from 1 to 88. That confirms the documented full-repaint flag cannot address the default/classic cases reported here.

The terminal consumer is unlikely to be the cause. cmux's Ghostty fork and current upstream have identical CSI n A clamping: oversized cursor-up is limited to the current row or top scrolling margin. The resulting corruption already exists in Ghostty/tmux screen-grid captures, meaning the terminal correctly applied an incomplete or wrongly anchored repaint stream and has no source frame from which to reconstruct the omitted cells.

Related cmux analysis: https://github.com/manaflow-ai/cmux/issues/5680#issuecomment-4997358769

There also appears to be a distinct 2.1.200+ tmux synchronized-output regression at https://github.com/anthropics/claude-code/issues/77689. That should not obscure this earlier native-cursor/frame-model regression.