[BUG] Scrollback duplication on terminal resize persists in 2.1.116 (VS Code integrated terminal, macOS)

Status Open
Reported on v2.1.101
Maintainer reply None cached
Activity 25 comments · opened Apr 22, 2026

Preflight Checklist

  • [x] I have searched existing issues and this hasn't been reported yet
  • [x] This is a single bug report (please file separate reports for different bugs)
  • [x] I am using the latest version of Claude Code

What's Wrong?

Update (2026-04-26)

This issue was originally filed against macOS + VS Code, but reports in this thread confirm it is not platform-specific and not VS Code-specific. Reframing scope and consolidating findings at the top. Original report preserved below.

Confirmed reproductions

  • macOS + VS Code integrated terminal (original report, also @Danielevko)
  • Linux + tmux + alacritty (@napter)
  • Linux + tmux + kitty (@igor47)
  • Linux + tmux + xterm.js web client (@eyalev)
  • Windows Terminal, legacy conhost.exe, Linux Mint + gnome-terminal (@NatanaelRibF)
  • Windows, generic (@cesarvarela)

Regression bisected to 2.1.101

@eyalev ran a deterministic harness across every native build on downloads.claude.ai from 2.1.100 → 2.1.119:

| Version | Leak count on 6 resizes |
|---------|--------------------------|
| 2.1.100 | 0 (clean) |
| 2.1.101 | 6 (regression lands) |
| 2.1.116 | 6 (claimed fix — unchanged) |
| 2.1.119 | 6 (current) |

The 2.1.116 changelog entry "Fixed scrollback duplication" does not address this variant. Last known-good version is 2.1.100 (2026-04-10).

Triggers (broader than resize)

The duplication fires on any main-screen redraw event, not just SIGWINCH:

  • Terminal resize
  • Output bursts that exceed pane height
  • ※ recap: away-summary block rendering on session resume
  • Input prompt state transitions (e.g. vim-mode -- INSERT -- indicator appearing/disappearing)

Suspected cause (per @napter)

Ink's redraw emits cursor-up + erase-line to rewind over the previous frame. When the dynamic region is taller than the viewport, cursor-up saturates at the top of the viewport — content that has already scrolled off cannot be erased. The next frame appends at the bottom and pushes one pane-height of un-erased content into scrollback. N redraws → N duplicates, spaced by pane height. Full-height panes that fit the entire dynamic region do not duplicate, consistent with the saturation theory.

Workarounds, ranked

| Workaround | Effect | Cost |
|---|---|---|
| Downgrade to 2.1.100 | Fully clean | Lose newer features |
| "tui": "fullscreen" in ~/.claude/settings.json | Eliminates leak (alt-screen renderer doesn't touch scrollback) | Laggier line-by-line scrolling, no native scrollback |
| "awaySummaryEnabled": false | Removes the recap trigger only | Doesn't fix resize or large-output triggers |

Confirmed not to help

  • VS Code GPU acceleration / useAtlasEngine: false on Windows Terminal
  • Shell choice (zsh / bash / pwsh / cmd / Git Bash)
  • Shell integration on/off
  • Custom statusLine on/off
  • editorMode (vim vs normal)
  • Font ligatures, integer fontSize, zero letterSpacing
  • Scrollback buffer size
  • Terminal smoothing / accessibility settings

Related issues

#51418, #46834, #52825, #52924, #52027, #52547, #52124

Ask

  • Re-triage as cross-platform (remove platform-specific labels)
  • Reopen if closed against the 2.1.116 fix
  • Diff 2.1.100 → 2.1.101 in the Ink / main-screen renderer path

---

Original report

The scrollback duplication on resize bug noted as fixed in v2.1.116 still reproduces in my environment. Each terminal resize during a Claude Code session pushes a full snapshot of the current TUI into terminal scrollback. Multiple resizes produce stacked duplicates of the splash screen / current view.

The fix from v2.1.116 ("Fixed scrollback duplication in inline mode where resizing the terminal or large output bursts would repeat earlier conversation history") appears to be incomplete for this environment combination.

Likely related to / possible duplicate of #18493.

Environment

  • Claude Code: 2.1.116
  • VS Code: 1.116.0 (integrated terminal, panel location)
  • OS: macOS 26 (Darwin 25.4.0)
  • Hardware: Apple Silicon
  • Shell: zsh (default macOS, minimal .zshrc — PATH exports only, no custom prompt)
  • Terminal font: JetBrainsMono Nerd Font Mono
  • TUI mode: default (the bug does not occur with tui: "fullscreen")

What Should Happen?

Expected: Scrollback shows whatever was in the shell before claude was launched.

Error Messages/Logs

**Actual:** Scrollback contains multiple stacked duplicates of the Claude splash screen, separated by stray characters (e.g. `)`).

The bug appears on the splash screen with zero user input, ruling out hooks, statusline, or any per-turn behavior.

Steps to Reproduce

Reproduction

  1. Open VS Code integrated terminal
  2. Run claude to start a fresh session
  3. Before sending any prompt, resize the terminal panel several times (drag the panel border, or toggle panel maximize)
  4. Scroll up in the terminal

Cross-platform reproducer (per @napter, Linux + tmux):

  1. Split a tmux pane so Claude Code runs in a pane ~50 lines tall
  2. Prompt a response that emits long tool-call output (e.g. a git commit with a HEREDOC message ≥60 lines, or a large diff)
  3. tmux capture-pane -p -S -50000 after the response completes — same content block repeats at intervals matching pane height

Claude Model

Opus

Is this a regression?

Yes, this worked in a previous version

Last Working Version

2.1.100 (regression introduced in 2.1.101, 2026-04-10)

Claude Code Version

2.1.116 (also reproduced on 2.1.117, 2.1.118, 2.1.119)

Platform

Anthropic API

Operating System

macOS (also reproduced on Linux and Windows — see Update section above)

Terminal/Shell

VS Code integrated terminal (also reproduced in alacritty, kitty, Windows Terminal, conhost, gnome-terminal, xterm.js)

Additional Information

Workaround

Setting "tui": "fullscreen" in ~/.claude/settings.json eliminates the glitch (alt-screen renderer doesn't touch scrollback). However, scrolling inside fullscreen mode is line-by-line and feels noticeably laggier than native xterm.js scrolling, so it isn't a comfortable permanent solution for heavy terminal users.

The cleanest workaround is downgrading to 2.1.100 until the renderer regression is addressed.

Ruled out (not the cause)

Exhaustively eliminated as causes across multiple reporters and platforms:

  • VS Code terminal.integrated.gpuAcceleration (on, off, auto — same)
  • VS Code terminal.integrated.shellIntegration (disabled — same)
  • VS Code terminal.integrated.suggest.enabled (disabled — same)
  • VS Code terminal.integrated.stickyScroll.enabled (disabled — same)
  • VS Code terminal.integrated.smoothScrolling (disabled — same)
  • VS Code terminal.integrated.minimumContrastRatio (removed — same)
  • VS Code terminal.integrated.scrollback (varied 1000–20000 — only changes how long duplicates persist before aging out)
  • VS Code editor.accessibilitySupport: "on" (set to off — same)
  • Windows Terminal useAtlasEngine: false (same)
  • Terminal font (swapped to Menlo — same)
  • Shell choice (zsh / bash / pwsh / cmd / Git Bash — same)
  • Custom statusline (removed — same)
  • editorMode (vim vs normal — same)
  • Custom hooks (logically excluded — they fire on Stop/Notification, but the bug appears pre-turn)
  • Plain zsh (no Claude) — resize + scroll works perfectly with all the above settings unchanged

Notes for triage

The bug only manifests when Claude Code is the running process. Same terminal config + same shell with anything else (vim, less, plain zsh) behaves correctly. The leak is in Claude Code's main-screen renderer's response to redraw events (SIGWINCH, large output, recap render, prompt-state transitions) — not in the host terminal or the shell.

View original on GitHub ↗

25 Comments

Danielevko · 4 months ago

happend to me since yesterday as well

Danielevko · 4 months ago

Reproducing this on v2.1.117 (one patch newer than OP) in the same VS Code integrated terminal + macOS combination. Fix is still incomplete.

Environment:

  • Claude Code: 2.1.117
  • VS Code integrated terminal (bash)
  • macOS 26.3.1 (Darwin 25.3.0), Apple Silicon

Additional trigger observed: the duplication does not require a manual SIGWINCH (resize). In my sessions it also fires after the "※ recap:" away-summary block renders, and when the input prompt transitions between states (e.g. the vim-mode -- INSERT -- indicator appearing/disappearing). Each of these events leaves a full copy of the bottom chrome (status line separators, statusLine command output, token count, input prompt) stacked in scrollback.

Already ruled out on my end (matching OP's list): custom statusLine (disabling it does not help), editorMode: "vim" vs "normal", terminal.integrated.gpuAcceleration: "off", fontLigatures.enabled: false, integer fontSize, zero letterSpacing, awaySummaryEnabled: false.

Only workaround that works: "tui": "fullscreen" — confirming OP's finding. Default-mode renderer is still leaking frames on 2.1.117.

Worth flagging: #51418 bisected this regression to v2.1.101. v2.1.100 is the last known-good version. That aligns with the Ink renderer change OP references.

Danielevko · 4 months ago

Follow-up: I was able to stop the duplication on v2.1.117 in default TUI mode (no "tui": "fullscreen") by adding "awaySummaryEnabled": false to ~/.claude/settings.json.

In my case the duplication was firing specifically when the "※ recap:" away-summary block rendered after returning to the session — the recap render appears to be one of the triggers that makes the main-screen renderer leak a full frame into scrollback. Disabling the recap removes that trigger and the bug has not reappeared.

This is obviously a workaround, not a fix — the underlying renderer issue is still there and would presumably still fire on resize or other redraw events. But for anyone who can live without the away-summary feature, this is a less disruptive workaround than switching to fullscreen TUI.

cslovell · 4 months ago

I had a similar issue - closed as likely duplicate of this.

napter · 4 months ago

Confirming this reproduces on Linux + tmux + alacritty, v2.1.118 — so it's neither macOS-specific nor VS Code-specific; the platform: labels undersell it.

Trigger: content exceeding pane height, not resize. This matches the "large output bursts" trigger the v2.1.116 fix was meant to address — that path is still broken.

Environment

  • Claude Code 2.1.118
  • tmux 3.x inside alacritty, Ubuntu 25.04
  • TERM=tmux-256color, default-terminal tmux-256color
  • TUI mode: default

Reproducer

  1. Split a tmux pane so Claude Code runs in a pane ~50 lines tall
  2. Prompt a response that emits a long tool-call output (e.g. a git commit with a HEREDOC message ≥60 lines, or a large diff)
  3. tmux capture-pane -p -S -50000 after the response completes

Signature (diagnostic): the same content block repeats at intervals that match the pane height almost exactly. In one 52-line pane I found 8 copies of ● Commit 554cea93 is in... at scrollback lines 2176, 2235, 2301, 2355, 2410, 2464, 2517, 2569 — gaps of 53–66 lines. Quick check for anyone reproducing:

tmux capture-pane -p -S -50000 | awk 'NF>20{c[$0]++} END{for(l in c) if(c[l]>=4) print c[l]" × "substr(l,1,80)}' | sort -rn | head

Any line repeating ≥4 times with spacing ≈ pane_height is this bug.

Hypothesis matching the spacing: Ink's redraw emits cursor-up + erase-line to rewind over the previous frame. When the dynamic region exceeds pane height, cursor-up saturates at the top of the viewport — it can't rewind past the top — so the portion that has already scrolled off screen is never erased. The next frame appends at the bottom and pushes exactly one pane-height of un-erased content up into scrollback. N redraws ⇒ N duplicates, spaced by pane height.

Full-height panes (88+ lines) in the same session do not duplicate, consistent with the saturation theory: if the full dynamic region fits in the viewport, cursor-up never saturates.

Does not reproduce with tui: "fullscreen" (alt-screen avoids scrollback entirely) — matches OP's workaround.

cesarvarela · 4 months ago

Happens in Windows, too.

At this point, I'm looking forward to what bugs Anthropic has in store for us next week.

NatanaelRibF · 4 months ago

Confirming this is not fixed in 2.1.119 and is not VS-Code-specific:

  • Claude Code 2.1.119, Windows 10 Pro 19045, Windows Terminal (MS Store latest)
  • Resize of the terminal window during a session reliably triggers duplicate rendering of recent scrollback (2–3× stacked)
  • Also reproduced in legacy conhost.exe on the same machine
  • Also reproduced on Linux Mint + gnome-terminal on a separate machine

Workarounds that did not help (all tested on Windows Terminal):

  • "useAtlasEngine": false in profiles.defaults
  • ❌ Disabling Session recap via /config
  • ❌ Different shells (PowerShell / cmd / Git Bash)

Matches the fullscreen-vs-inline renderer split reported in #52825 — tui: "fullscreen" masks the symptom but sacrifices native scrollback. The v2.1.116 changelog fix for inline-mode resize duplication remains incomplete at 2.1.119.

Related tracking: #52924, #52027, #52825, #52547.

igor47 · 4 months ago

i can confirm @napter 's exact issue, using 2.1.119 / tmux 3.6a / kitty

eyalev · 4 months ago

Confirming this finding from a different platform.

Linux x64 + tmux + xterm.js web client — 2.1.116's "Fixed scrollback duplication" changelog entry does not fix the variant this issue (and this report) reproduces.

I ran a deterministic harness (launch CC, 6 tmux resize-window calls, count duplicate UI frames in scrollback) across every native build available on downloads.claude.ai from 2.1.100 through 2.1.119. Result:

| Version | Leak count on 6 resizes |
|---------|--------------------------|
| 2.1.100 | 0 (clean) |
| 2.1.101 | 6 (regression lands) |
| 2.1.116 | 6 (claimed fix — unchanged from 2.1.101) |
| 2.1.119 | 6 (current) |

3/3 reproducible. So the bug pre-dates 2.1.116 — the fix in that release was never applied to (or didn't cover) this variant. Bisects exactly to 2.1.101 (2026-04-10).

Posting the full bisection table + repro on the broader issue at #46834.

r1ckrck · 4 months ago

Consolidating thread findings and updating the issue body to reflect cross-platform scope.

Cross-platform reproductions: macOS + VS Code (original), Linux + tmux + alacritty (@napter), Linux + tmux + kitty (@igor47), Linux + tmux + xterm.js (@eyalev), Windows Terminal + conhost + gnome-terminal (@NatanaelRibF), Windows generic (@cesarvarela). Not macOS-specific, not VS Code-specific.

Regression bisected to 2.1.101 by @eyalev — last good version is 2.1.100 (2026-04-10). Leak count unchanged from 2.1.101 through 2.1.119, including 2.1.116 where the changelog claims a fix.

Triggers are broader than resize: also fires on output exceeding pane height, the ※ recap: block rendering on resume, and vim-mode indicator transitions. @napter's hypothesis — cursor-up saturating at viewport top during Ink redraws — fits the observed pane-height-spaced duplicates.

Workarounds, ranked:

  1. Downgrade to 2.1.100 — clean
  2. "tui": "fullscreen" — eliminates leak, laggier scrolling
  3. "awaySummaryEnabled": false — only fixes the recap trigger

Request: please re-triage with cross-platform scope and reopen if this was closed against the 2.1.116 fix. Issue body updated with the full details.

mustafaelshobaky-allcare · 4 months ago

I tried to downgrade to 2.1.112 and it worked fine after that on my Mac.
I resize the windows and the duplication no longer happens.
I didn't need to downgrade to 2.1.100

parth-io · 4 months ago

Found a workaround in #45856, set CLAUDE_CODE_NO_FLICKER=1 in your ~/.claude/settings.json:

  "env": {
    "CLAUDE_CODE_NO_FLICKER": "1"
  },
Jack-261108 · 3 months ago

I can reproduce what looks like the same resize/reflow corruption on macOS + Ghostty with Claude Code 2.1.144.

Repro:

  1. Ask Claude Code to implement bubble sort in Java.
  2. Let it render the Java code block.
  3. Resize the terminal window.
  4. The existing code block is redrawn with one line duplicated many times, making the code block look corrupted.

Observed duplicated line:
for (int j = 0; j < arr.length - 1 - i; j++) {

Environment:

  • Claude Code: 2.1.144 (Claude Code)
  • OS: macOS 15.7.7 / Darwin 24.6.0
  • Terminal: Ghostty (TERM_PROGRAM=ghostty, TERM=xterm-ghostty)
  • Shell: zsh

Expected: resize should reflow/redraw the existing code block without adding duplicate lines.
Actual: the same code line is repeated many times after terminal resize.

Mike-E-Log · 3 months ago

Confirming this still reproduces on 2.1.154 (newest) — Windows 11 / Windows Terminal / PowerShell — and adding a render-only proof, a standalone repro, and an inline-preserving fix direction.

@napter's cursor-up-saturation theory and @eyalev's 2.1.100→2.1.119 bisection match what I see exactly. Extending the data forward and adding three things the thread doesn't have yet.

1. Still broken on 2.1.154 (Opus), Windows 11, Windows Terminal + PowerShell, inline mode. The bisection stopped at 2.1.119; it persists on the current build. The only non-fullscreen scrollback-dup fixes in the changelog are 2.1.120 ("Fixed terminal scrollback duplication in non-fullscreen mode (resize, dialog dismiss, long sessions)") and 2.1.121 ("…pressing Ctrl+L or triggering a redraw … on tmux, GNOME Terminal, Windows Terminal, and Konsole"); nothing since names the height precondition. (Consistent with your "custom statusline — same" ruling-out: I independently cleared hooks and the statusline too — it's purely the renderer.)

2. A concrete instance of your "any main-screen redraw" triggers, with no resize: a confirmation/permission dialog rendering below a tall response duplicates the response above it. Same class as your "input prompt state transitions," just a very reproducible one — painting the dialog forces a redraw while the live region exceeds the viewport, and the response stacks 3–4× in scrollback. No window move, no Ctrl+L. (I also see your resize/re-wrap variant — duplicate copies re-wrapped at different widths.)

3. Render-only proof (the model emits once): the multi-painted on-screen block maps to exactly one saved assistant message in the session .jsonl — each unique line appears once on disk, and a scan across ~20 recent transcripts found zero duplication in saved messages. So this is purely the inline repaint, not model/transcript output. (A cheap, scriptable way to separate render-dup from content-dup during triage.)

4. Standalone repro (no Claude Code) corroborating @napter's saturation theory at the escape-sequence level: a ~60-line Node script prints a frame taller than the viewport, then repaints via cursor-up (CSI nA) + erase-line (CSI 2K). Cursor-up clamps at the viewport top — Microsoft's Console VT docs, verbatim: "Cursor movement will be bounded by the current viewport into the buffer. Scrolling (if available) will not occur." — so off-screen rows are unreachable and each redraw appends a fresh un-erased copy. Byte-level it's deterministic (N rewinds → N+1 content copies); visually it's height-gated (run in a window shorter than the frame). Happy to attach.

Inline-preserving fix direction — since both listed workarounds have a real cost (downgrade loses features; tui: fullscreen loses native scrollback): keep oversized finished output out of the redraw cycle entirely.

  • Commit completed turns to Ink <Static> — written to the main buffer once, never repainted — so a dialog or a re-wrap can't touch already-committed content (and it stays in native scrollback, unlike alt-screen).
  • Clip the small live tail to stdout.rows so cursor-up can't saturate.
  • Drop ESC[3J ("Erase Saved Lines," an xterm extension — the Windows ED docs define only params 0/1/2) from the clearTerminal path so a full repaint can't wipe scrollback either.
parth-io · 3 months ago

Just use /tui fullscreen or set CLAUDE_CODE_NO_FLICKER=1

The old renderer won't be fixed, when Anthropic itself switched to the new one completely

vdavid · 2 months ago

Nice, /tui fullscreen completely solved it for me. The context usage indicator disappeared, though, but the I asked Claude to

/statusline 🧠 Opus 4.8 high | 📁 {cwd} | 🌳 {worktree} | 💾 614k/1M (67%) | 📅 54% | v2.1.165 (the calendar thingie is my weekly token usage) - add some reasonable colors to it too

and now it's very nice again.

ojura · 2 months ago

Filed #66277 with a root-cause analysis and a self-contained repro for the resume + overflow variant of this.

In inline mode the frame buffer height is the full content height (renderer.ts: height = altScreen ? terminalRows : yogaHeight), so committed history lives in both the repaintable frame and native scrollback at once. Any settle that grows rendered height re-runs the writer over the overflowing buffer and re-emits the row at the scrollback/viewport seam, which the terminal cannot clear from scrollback. That single mechanism seems to cover the trigger list at the top here: resize moves the seam, output bursts and recap-on-resume grow the frame, prompt-state transitions change height.

The repro in #66277 strands the seam row on a freshly generated overflowing session with no recap block, on pristine 2.1.157 and 2.1.168; a tall control is clean. Notably, restoring the growing full-reset and suppressing the recap/notice both fail to fix it; only removing the overflow yields zero copies. Suggested direction: bound the live frame to the viewport and commit scrolled history to scrollback exactly once (a real <Static> / print-once channel), so no row exists in both.

Yan-Yu-Lin · 1 month ago

Hi all — sharing something that might help here.

Like a lot of people, the non-fullscreen renderer breaking is what finally pushed me over the edge, and like a lot of you, I refuse to switch to NO_FLICKER=1 / fullscreen just to get clean output — losing native scrollback is a worse deal than the bug.

The diagnosis in this thread is spot-on. So I took the fix it describes — emit history once, don't re-lay-out already-committed rows, redraw cleanly — and actually implemented it, by byte-patching the native binary (the app's JS is bundled inside; you can edit it in place with same-length patches and re-sign). An AI agent did most of the low-level digging.

What the patched build does, in normal (non-fullscreen) mode:

  • Ctrl+O expands the whole transcript, not just the recent slice
  • Ctrl+O again collapses everything and cleanly re-renders
  • resize does one clean full repaint instead of stacking duplicate copies

Honest scope: macOS / Apple Silicon (arm64), version-specific (done for 2.1.203 and 2.1.207). It patches a copy and installs as a separate command, so your normal claude is untouched. It's a hack, not an official fix, and the trade-offs are written up in the repo. Linux/Windows aren't built yet, but the same JS is inside every platform's binary, so the repo explains how you (or your own AI agent) can reproduce it on your OS and re-apply it after updates — there's a playbook and a verify harness.

Repo: https://github.com/Yan-Yu-Lin/claude-code-renderpatch

If it works for you, a ⭐ on the repo helps me gauge who's finding it useful — and if it doesn't work, or you hit a snag on your platform/version, open an issue there and I'll take a look.

---
Note: this comment was drafted with Claude — English isn't my first language, so I had the AI help me put it into words. The patch and the repo are my own work.

oviano · 1 month ago

Adding a data point from the terminal-emulator side — we implement a terminal that captures the raw PTY byte stream, so this is confirmed from the sequences themselves rather than inferred from pixels.

Repro shape: run a session at a wide width, resize to ~60 cols so the Ink dynamic region wraps taller than the viewport, let a few frames render, then resize back to wide.

What the byte stream shows: on each redraw at the narrow width, the cursor-up rewind saturates at the top of the viewport once the dynamic region exceeds the screen height. Rows above the saturation point are never erased and scroll into history as ordinary committed rows — a partial stale frame (spinner/status lines frozen mid-state) plus the blank padding rows between the transcript tail and the status area. The next frame renders below, so scrollback permanently contains: stale frame copy → a run of ~10 blank rows → fresh content, with the two copies not byte-identical (spinner advanced, task list changed between frames).

It compounds across resizes: in one real session that went through several width changes, the same status table appears three times in scrollback, each copy rendered at a different width (one clipped mid-word, one narrower, one clean) — i.e. the overflow commit fires once per resize step, fossilizing a stratum of the frame at each width.

Worth stressing for triage: no terminal emulator can fix this faithfully. The stale copies are legitimately committed output — reflow preserves them, and any dedup aggressive enough to remove near-identical-but-diverged frames would be deleting real history. The fix has to be at the renderer (erase the full dynamic region before redraw when it exceeds the viewport, or clamp the region to viewport height).

Happy to share raw PTY captures of the exact sequences if useful.

nerves76 · 21 days ago

The "cursor-up saturates at the top of the viewport" hypothesis in this issue's body is correct. We captured it at the byte level and can confirm the exact mechanism, measured from a raw PTY recording of Claude Code 2.1.220 (classic renderer, 131×25, TERM=xterm-256color).

The anatomy of one corruption event:

  1. With the cursor on row 22 (0-based — the 23rd row of a 25-row screen), the renderer emits ESC[24A: cursor up 24, with only 22 rows above the cursor.
  2. Per VT spec, CUU clamps at the viewport top. The entire repaint that follows lands 2 rows higher than the renderer believes, welding the new frame over the wrong rows.
  3. The repaint is column-addressed (ESC[nG positioning) with no EL/erase, so every column the new frame doesn't explicitly write survives from the old frame — in our capture, stale characters persist in 14 skipped columns of the welded row.
  4. The welded rows scroll into scrollback and are permanent. Later clean reprints append below; nothing can retire the corrupt copy.

Why this is the renderer, not the terminal: we replayed the identical byte stream through three unrelated implementations — tmux 3.6a (capture-pane scrollback read), pyte, and SwiftTerm — and all three commit the identical welded state to history, byte-for-byte. Any VT-conformant emulator must: the mid-frame state genuinely exists in the stream at the moment rows scroll out.

The statistic: a scanner that tracks cursor position across the full stream and flags any CUU larger than the rows above the cursor finds exactly one clamped CUU in the corrupted capture (of 793 CUUs total — at the precise byte offset where the weld appears) and zero in eleven clean captures (50–1,339 CUUs each). Two independently-written scanners (one stdlib-Python, one pyte-based) converge on the same offset and row index. Mechanism inferred from n=1 corruption event, stated as such — but the 1-vs-0 split across twelve captures is clean.

This also connects two sibling reports: the missing erase is #84297's measurement (rows repainted without EL), and #84247's explicitly-unverified guess ("without accounting for how many of those rows have already scrolled out") is this mechanism exactly. #60069's decompile found the absolute-positioning path already clamps (Math.min(Math.max(h.y+1,1), q)) — the relative CUU path needs the same treatment, with one crucial difference: when the desired rewind exceeds rows-above-cursor, clamping the move is not enough — the renderer must also drop the rows that have scrolled away from its repaint plan, or the frame still lands misaligned relative to its own content. (Upstream Ink has the same structural issue in log-update's eraseLines(previousLineCount) — no clamp against stdout.rows anywhere — see vadimdemedes/ink#973.)

Happy to share the raw fixture, the scanner script, and the replay harness if useful to a maintainer.

harshalgajjar · 18 days ago

The mechanism this thread has converged on — cursor-up rewind saturating at the viewport top, confirmed at the byte level by @oviano and @nerves76 — is not Claude-Code-specific. It's the rendering model of Ink, the framework Claude Code's renderer is built on, and any Ink-style renderer inherits one of two failure modes the moment the dynamic frame exceeds the terminal height:

  • stock ink@7.1.1 today: clears the terminal with ESC[2J ESC[3J ESC[H on every re-render while overflowing — which erases the user's entire scrollback, including shell history from before the app started (measured: 12 wipes in 4 seconds on a ticking app; one resize deleted 102 lines of pre-existing history);
  • Claude Code's renderer: avoids the ESC[3J (preserving history), so the unreachable rows above the saturation point are re-emitted instead — the duplicate-stamping documented in this issue. Our PTY captures of 2.1.220 show the resize repaints contain zero 2J/3J, only ESC[H + full rewrites, one stale copy per resize.

We've now filed this upstream with byte captures, a minimal reproduction (plain Ink app + tmux resizes), and annotated GIFs of both failure modes: vadimdemedes/ink#990.

The fix direction proposed there resolves both modes at once and is equally applicable to Claude Code's fork without waiting for upstream: never rewind or rewrite past the viewport — when the frame is taller than the screen, clamp rendering to the bottom rows lines and treat everything that has scrolled off as immutable committed history (flushed once, like <Static> content). Content above the viewport is unreachable by cursor motion per the VT spec, so re-emitting it is what mints the duplicates, and ESC[3J is the only other exit — both symptoms, one cause.

jdanbrown · 18 days ago

But what about allowing reflow of the lines already baked into scrollback? This is something that codex does correctly right now but claude messes up.

Example of not-reflowed scrollback can be seen in this issue:

And past issues flagging the reflow bug specifically:

oviano · 18 days ago

Yes. It's the one thing that would improve Claude Code massively.

The capability is there because it reflows the whole scrollback on --resume, but they should do this on resize too.

harshalgajjar · 18 days ago

Agreed — and I think you've identified the deeper layer of the same bug. Frozen-at-emission-width scrollback (your links) and duplicate-stamping (this issue) share one root: the renderer pre-wraps. Our PTY captures confirm it — no emitted run ever exceeds the current column count; every line break in the transcript is a hard break baked in at render width.

That single decision causes both symptoms:

  1. Committed lines carry hard breaks, so the terminal can't reflow them on resize — cat output re-wraps when you widen the window; Claude's transcript can't (#84247 etc.).
  2. Because width-baked history can't re-fit, the renderer's only way to "fix" it on resize is to re-emit it at the new width — and once the frame is taller than the viewport, the cursor-up rewind saturates and each re-emission becomes a stamped duplicate instead of a replacement.

Codex avoids both by emitting the transcript as soft-wrapped logical lines: the terminal owns the wrapping, so scrollback reflows natively and history never needs re-rendering. Nothing to re-emit → nothing to duplicate.

So the complete fix at the renderer layer is: flush committed content exactly once, as logical lines without baked-in wrapping (hard-wrap only inside the live frame, where layout genuinely needs it). That yields commit-once (kills the duplication) and terminal-native reflow (your ask) as two halves of one change.

One data point on why this must happen in the renderer: we tried recovering logical lines downstream in a terminal implementation and it's not possible in principle — once wrapping is baked into the byte stream, the terminal can't distinguish a hard break from a soft one. Only the app knows its logical lines.

The fix shape is identical in vadimdemedes/ink#990, whose suggested directions now include the reflow angle.

oviano · 17 days ago

There is an advantage to re-rendering the history though - elements like tables get drawn differently at narrow widths etc.