Rows are repainted without erase-to-end-of-line, destroying output mid-paragraph
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.
4 Comments
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.@nerves76 — thank you, that composition is right, and it explains something my own measurement
could not. A missing
ELalone leaves a ghost that the next clean repaint overwrites; aclamped
CUUalone misaligns a frame that would otherwise be redrawn correctly. Only togetherdo 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:
CUU nmay exceed thenumber 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.
EL— or simply emit one unconditionally onrepaint, 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, nativeinstaller. 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.
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
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:
⎿ 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.⎿ exit=0and the real result — sits 300 rows further down.Two notes that may be useful:
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.