[BUG] Renderer paints stale frame past new viewport after SIGWINCH — cumulative \x1b[1B walks exceed new row count (relative-cursor path, alt-screen)

Open 💬 4 comments Opened May 17, 2026 by JBetchGH

Version

  • Claude Code v2.1.143
  • Linux (Ubuntu 24.04, Bun-compiled native binary)
  • Opus 4.7 (1M context)
  • Reproduced over an SSH-pty-bridge transport (meshTerm), but the bug

is geometry-only and should reproduce in any terminal that shrinks
during a session.

Summary

In an alt-screen Claude session that has accumulated meaningful context
(observed: ~1 h uptime, ~4 MB cumulative output), shrinking the terminal
geometry causes the post-SIGWINCH render to paint a frame taller than
the new viewport
. The result: split screen, top half is stale content
from the prior render at the old geometry, the prompt + recent output
fall past the visible viewport (often hidden behind the on-screen
keyboard in remote-terminal contexts). Recovery requires killing the
Claude process — Ctrl+C inside the wedged session does not unstick the
renderer, even though stdin still reaches the input parser.

Why this is a new report (not a duplicate)

Several adjacent open issues describe related symptoms but none captures
this mechanism:

  • #46834 — TUI relayouts spill duplicate transcripts into scrollback.

Different visual symptom; same renderer area.

  • #55762 — Feature request: force full TUI repaint after resize.

Notes "Ink only redraws the current frame on SIGWINCH" — adjacent.

  • #53394 — Resize causes duplication; rewind causes freeze. Notes the

"after a long session" correlation we also see.

  • #59913 — CLI hangs; UI swallows input. Explicitly states "SIGWINCH

still works" during that hang — orthogonal to this report.

What's new here: the exact byte-stream pattern that causes the wedge,
and a hypothesis pointing at a specific render/layout-pass ordering
race rather than at the streaming path or the scrollback duplication.

Evidence (PTY byte stream from a wedged session)

Captured from the host-side PTY ring buffer immediately after a
downward SIGWINCH (terminal shrunk from 40 → 23 rows while the
keyboard was being presented in the remote client). Excerpt of the
bytes emitted by Claude in the ~200 ms post-resize window:

\x1b[2C  \x1b[1B  \x1b[7C  \x1b[1B  \x1b[3C  \x1b[1B  \x1b[2C  \x1b[1B
  ↑        ↑        ↑        ↑        ↑        ↑        ↑        ↑
CUF 2    CUD 1    CUF 7    CUD 1    CUF 3    CUD 1    CUF 2    CUD 1

That is: only CUF (Cursor Forward) and CUD (Cursor Down)
relative-movement sequences, plus interleaved SGR (\x1b[38;5;231m)
color changes. Zero CUP / HVP absolute-position sequences in this
window.
The cumulative CUD count substantially exceeds the new row
count (23) — the renderer is walking down 40 rows of frame content
into a 23-row viewport.

Stack identification (from binary string-grep, since source isn't published)

The bundled binary contains:

  • reconcilerVersion:"19.2.0" — React 19.2.0 reconciler
  • __REACT_DEVTOOLS_GLOBAL_HOOK__ plumbing
  • calculateLayout, getComputedHeight, setMeasureFunc — Yoga

flexbox layout

This is Ink (vadimdemedes/ink).

Hypothesis (where to look)

The cursor-emission path in the binary shows two distinct functions:

// Absolute positioning (alt-screen path): row clamped to q (viewport rows)
let g = Math.min(Math.max(h.y + 1, 1), q);
let c = Math.min(Math.max(h.x + 1, 1), $);
G.push({ type: "stdout", content: sR(g, c) }); // CUP

// Relative positioning (between cells in a single render pass)
G.push({ type: "stdout", content: BZH(deltaX, deltaY) }); // CUF + CUD

The absolute path is clamped to q (current viewport row count),
so CUP sequences with row > q are not emitted — consistent with our
observation. But the relative path walks deltaY between visually-
distinct cells of the already-laid-out frame. If the frame was
laid out at the old row count and the post-SIGWINCH render pass fires
before Yoga's calculateLayout finishes re-measuring at the new row
count, the relative walk accumulates deltaY for a 40-row frame
emitted into a 23-row viewport.

The second render pass — after Yoga finishes — would correct it. But
if the user has stopped typing and Claude has stopped streaming, no
second render is scheduled. The wrongly-walked frame becomes the
stuck visual.

Suggested fix area

The post-SIGWINCH render should await Yoga's layout-clean signal
before emitting the next frame. Alternatively, the renderer could
clamp the total walked deltaY to q-1 at emission time, or
re-issue an absolute CUP at frame start to home the cursor before
relative walks begin.

A stronger heuristic for the test suite: after a SIGWINCH that
shrinks rows, the next frame's emitted byte stream must not contain
cumulative CUD motions exceeding the new row count.

Correlations (informally observed)

The bug appears reliably on sessions older than ~30 min OR with
cumulative output past ~few-MB. Short fresh sessions don't wedge.
Suggests Yoga layout time or React reconcile depth grows with
component count and tips past whatever timing budget the render
pipeline assumes.

Reproduction (terminal-agnostic)

  1. Launch claude in a terminal sized at e.g. 80×40.
  2. Run a substantial conversation that accumulates context (multiple

responses, tool calls, file reads). 30+ minutes is reliable.

  1. Shrink the terminal window to e.g. 80×23 (a keyboard appearing

on a remote-terminal client; a window resize on desktop; stty
rows 23
from another shell pointing at the same TTY).

  1. Observe: the prompt + recent output fall past the new visible

bottom edge. Scrolling the terminal does not bring them back
(the bytes were written below the viewport, not into scrollback).

  1. Recovery: kill the Claude process. claude --resume restores

the session intact, confirming the in-memory conversation state
isn't damaged — only the live render.

Adjacent telemetry

We've shipped a non-invasive detector on the host side
(meshtermd v0.9.3+) that watches PTY output for this pattern and
appends de-identified JSON records on detection — geometry numbers,
byte counts, session age, no user data. Happy to share the schema or
collect more samples if useful.

View original on GitHub ↗

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