Rows are repainted without erase-to-end-of-line, destroying output mid-paragraph

Status Open
Reported on v2.1.222
Maintainer reply ✓ Yes — bcherny
Activity 4 comments · opened Aug 5, 2026
💡 Likely answer: A maintainer (bcherny, collaborator) responded on this thread — see the highlighted reply below.

Summary

Claude Code moves the cursor back to a row it has already drawn and writes shorter content into
it without sending an erase-to-end-of-line (CSI K). The tail of whatever was in that row
survives on screen, gets mixed into the new content, and is destroyed by the next redraw. The
visible result is answer text that stops mid-sentence, or reassembles into gibberish.

This is not an emulator problem. A terminal cannot invent an erase the program never sent.

Environment

  • Claude Code 2.1.222 (measurements below recorded on 2.1.220; behaviour unchanged since)
  • Native installer (~/.local/share/claude/versions/<v>), not an npm global
  • Linux Mint 22.3, Cinnamon, X11
  • xfce4-terminal, TERM=xterm-256color, 150-column window
  • Reproduced across many sessions over several weeks; not tied to any one project or config

What was measured

I wrote a recorder that captures the raw byte stream Claude Code writes to the terminal and
models the screen from it, then reports how many revisited rows received an erase. It verifies
itself before each run: 0 ghosts on a known-good stream, exactly 1 (27 leftover characters) on a
known-bad one.

Two full ~4-hour sessions, same version, same terminal:

| | session A | session B |
|---|---|---|
| rows revisited (cursor moved back up) | 41,103 | 5,310 |
| erase-to-end-of-line sent | 5,158 (12.5%) | 770 (14.5%) |
| redraws that left a ghost | 20,594 | 2,345 |
| leftover characters | 812,890 | 77,326 |
| rows that lost text and never got it back | 24,750 | 3,027 |
| distinct destroyed lines, never written again | 2,209 | 64 |

So roughly seven of every eight repainted rows carry no erase.

The byte stream was validated against the model: no scroll-up/down, no reverse index, no
alternate screen, 2 scroll-region sets, 100 absolute cursor moves. The CLI redraws purely by
cursor movement, so a row model tracks reality here.

An earlier pass reported 21,469 ghosting redraws and 868,075 leftover characters. Those were
inflated by OSC-8 hyperlink sequences being counted as visible text; the table above is the
corrected reading.

What is actually destroyed

Splitting the "never written again" count by kind, on one 1.9-hour recording:

  • 1,216 rows (72.5%) were prose — real answer text
  • 350 rows (20.9%) were furniture: tool headers, box rules, the status bar
  • 112 rows (6.7%) were mash — a row that had two different lines written into it, reassembled

into gibberish

Every destroyed prose row measured 150–151 characters, exactly the terminal width — i.e. they
are mid-paragraph continuation rows. That is precisely the reported experience of "it cut my
sentence off."

The worst offenders are the spinner bullets and the status line. The commonest single case is
the status line being painted over a row that still holds answer prose: it lands short, the prose
tail ghosts underneath it, and the next redraw destroys the composite.

A second symptom, probably the same cause

Text the user types can also vanish from the prompt line. There is a sharp
works-here/fails-there boundary worth pointing at: in the same session, same terminal, same
version, typing echoes back correctly inside a permission dialog's addendum box but not at the
main prompt. Whatever differs between those two input paths is where that half lives.

Expected vs actual

Expected: a row being repainted with shorter content is cleared first, so nothing of the
previous content survives.

Actual: the shorter content is written over the start of the row and the remainder of the old
row stays on screen.

Suggested fix

Emit CSI K (erase to end of line) when repainting any row whose new content is shorter than
what was previously drawn there — or unconditionally on repaint, which is cheap. The status-line
and spinner renderers are the highest-value places to start, since they account for most of the
ghosting.

What I can provide

Raw recordings, the per-session breakdowns, and the recorder itself, if any of that would help.
Rates vary enormously with workload — two sessions of the same version differed tenfold in
ghosted characters per hour — so single-session comparisons are not meaningful; the tables above
are from comparably heavy sessions.

View original on GitHub ↗

4 Comments

nerves76 · 21 days ago

Your missing-EL measurement is one of two halves of the scrollback-corruption mechanism now confirmed byte-level on #51828 (https://github.com/anthropics/claude-code/issues/51828#issuecomment-5233074519): a clamped ESC[24A (cursor-up larger than rows-above-cursor) misaligns the whole repaint, and the missing erase you measured is what lets the old frame's characters survive inside the welded rows. The two defects compound: either alone is mostly cosmetic; together they weld permanent corrupt rows into scrollback.

nynjapirate · 17 days ago

@nerves76 — thank you, that composition is right, and it explains something my own measurement
could not. A missing EL alone leaves a ghost that the next clean repaint overwrites; a
clamped CUU alone misaligns a frame that would otherwise be redrawn correctly. Only together
do they weld a corrupt row that nothing can retire. My data has always shown far more ghosting
than permanent loss (20,594 ghosting redraws against 2,209 rows destroyed for good in one
session), and the ratio between those two numbers is exactly what a rare clamp event predicts.

Adding a longitudinal result, since it bears on how any fix here gets verified.

The defect is unchanged across eleven releases

63 rated recordings, 454 hours of session time, versions 2.1.220 through 2.1.231, all on the
classic renderer, same machine, same terminal, same 150-column window.

No version-to-version change is detectable. Every version's median lies inside the range a
single version shows against itself — 2.1.227 alone (n=14) spans 2.7 to 1,832.6 destroyed rows
per hour. 2.1.229's higher median (678 against 220 overall) is not a regression: I tested
session duration as the confound and rejected it (r = −0.20; medians binned by duration come out
241 / 315 / 190 / 210, i.e. flat). The within-version spread swallows it.

Total spread within this corpus: 1,833-fold on destroyed rows per hour, 8,100-fold on
ghost characters per hour.

Correcting my own earlier figure

In an earlier note I proposed a "normal band" for these rates, fitted to nine recordings of
2.1.220. That band was too narrow and I am withdrawing it — across the full corpus four
recordings sit above its ceiling, the worst at 1,832.6 destroyed rows and 591,291 ghost
characters per hour. Anyone who picked those numbers up should drop them.

Why this matters for triage

The consequence is uncomfortable but worth stating plainly: rate measurement cannot demonstrate
that this bug has been fixed.
The measure cannot resolve a change smaller than roughly ten-fold,
and it gets less sensitive the more data is collected, because every additional recording widens
the observed normal range. Ghosting rate tracks how hard the screen was redrawn — agent output,
tool volume, spinner time — far more strongly than it tracks the renderer version.

So a before/after pair of sessions across a release cannot settle anything, in either direction,
and I would gently suggest nobody try to close this on that evidence. What can settle it is
cheap and deterministic, and it is what @nerves76's scanner already does:

  1. Assert in the renderer, or in a test harness reading the PTY: no CUU n may exceed the

number of rows above the cursor. That is 1-vs-0 across twelve captures in their data, and
1-vs-0 is a signal a rate never will be.

  1. Assert that every repainted row receives an EL — or simply emit one unconditionally on

repaint, which costs three bytes.

Both are pass/fail on a single recorded stream. Neither needs a corpus.

Environment

Linux Mint 22.3, Cinnamon, X11, xfce4-terminal, TERM=xterm-256color, 150 columns, native
installer. Measurements above end at 2.1.231; 2.1.232 is installed here but not yet rated, and I
have not yet run the fullscreen-renderer arm that others in #51828 report as a workaround.

Raw recordings, the per-version table, and the recorder itself remain available to anyone at
Anthropic who wants them.

bcherny collaborator · 5 days ago

Reproduced the core measurement on 2.1.233. I captured the raw byte stream of an interactive session (150-column terminal, long streaming prose answers) and modeled the screen from it with a recorder that self-verifies on known-good/known-bad streams, similar to yours. In that session, of 503 repaints of rows the cursor had moved back up into, only ~19% carried an erase-to-end-of-line; 86 repaints wrote shorter content over a longer previously-drawn row with no erase, leaving several thousand stale characters behind. The single most common ghosting case in my capture matches yours exactly: the spinner/status row (~20–25 columns wide) painted over a row still holding a full-width 150-character prose continuation line, with the prose tail surviving underneath. Most repaints do cover the old row by overwriting with trailing spaces, which is why the screen usually ends up correct — but the spinner/status paths paint short without clearing, which is the window where the mid-paragraph corruption you documented can occur. In my short session the final screen state recovered, so I confirmed the missing-erase mechanism rather than the permanent "destroyed lines" end state, which your multi-hour recordings capture; those recordings and the recorder would definitely be useful if you're still able to share them. Thanks for an unusually rigorous report.

🤖 Generated with Claude Code

nerves76 · 1 day ago

Still present on 2.1.250, confirmed today from an independent terminal implementation.

We maintain a third-party macOS terminal (SwiftTerm-based) and have been tracking this from the terminal side. Adding a data point rather than a new report, since the mechanism here matches what we captured at the byte level on #51828 back on 2026-08-09.

Observed on 2.1.250 in an ordinary long session:

  • A superseded in-progress frame welded into scrollback mid-transcript — a tool block ending ⎿ Running… (7m 39s · timeout 10m) with the live spinner line still attached, frozen at row 369 of a 753-row capture, with ~380 rows of later content below it.
  • The same block's completed copy — ⎿ exit=0 and the real result — sits 300 rows further down.
  • The two copies share an identical head and differ only in the tail, which is the signature we documented previously.

Two notes that may be useful:

  1. The version matters for triage. The reproductions recorded in this thread and its siblings are on 2.1.220 and 2.1.233; this is 2.1.250, so nothing shipped between those has closed it.
  2. From the terminal side there is no fix available to us. Once the duplicate rows are in the byte stream they are indistinguishable from intentional output, and they are already committed to scrollback by the time they scroll off. We closed out every terminal-side avenue before concluding that.

For what it's worth, the direction proposed in vadimdemedes/ink#990 — flush committed content once, as logical lines without baked-in wrapping, and never rewind past the viewport — matches what we would need as a terminal. Committed-once output is what makes native reflow possible downstream; anything re-emitted is something we cannot tell from new content.

We carry a standing regression test that fails today and will start passing the moment this closes, so we can confirm a fix quickly whenever there is something to test against.