[BUG] TUI renderer + stdin die mid-session while the engine keeps running (macOS, 2.1.207) — idle CPU, no SIGWINCH redraw, transcript still being written
Environment
- Claude Code 2.1.207
- macOS 26.5.1, Apple Silicon (arm64)
- Reproduced under both Ghostty 1.3.1 and tmux 3.7b → terminal-independent
- Interactive TUI,
/tui fullscreenmode
Summary
Mid-session, Claude Code's terminal UI dies completely — it stops rendering and stops accepting keyboard input — while the engine underneath keeps running normally for hours: it continues appending to the session transcript and continues driving in-process background agents to completion.
This is not a hang. Nothing is blocked. The UI simply stops being driven. The window is unrecoverable in place; the only way out is to close it and claude --resume <session-id>, which works reliably (only the unsent input line is lost).
It happened twice in one day to the same conversation, in two different terminal stacks.
Precise signature (this is the useful part)
Measured on the live frozen process:
| Observation | Result |
|---|---|
| Process state / CPU | alive, S, ~1% CPU — not a spin |
| sample <pid> | main thread in kevent64; workers in psynch_cvwait; zero threads in any read or write syscall |
| stdout byte offset (lsof -o, fd 1) | frozen — the process writes nothing |
| kill -WINCH <pid> | no redraw — offset unchanged before and after |
| Session .jsonl transcript | still being appended (engine alive) |
| Background-agent message bus | still draining (engine alive) |
| Symbolized stacks | unresolvable — ??? (in 2.1.207) (stripped binary) |
Anyone can reproduce the observation on a frozen session with:
off() { lsof -o -p "$1" | awk '$4 ~ /^1[urw]/ {print $7}' | head -1; }
o1=$(off $PID); sleep 3; o2=$(off $PID)
kill -WINCH $PID; sleep 2; o3=$(off $PID)
echo "$o1 / $o2 / $o3" # all identical => app has written nothing, even after a resize signal
A healthy Claude Code TUI always repaints on SIGWINCH. A frozen one emits zero bytes.
Interpretation: because no thread is blocked in a syscall and CPU is idle, the renderer/stdin layer was not stalled — it appears to have been torn down (or thrown out of) and never restarted, while the engine's event loop carried on unaware. We cannot go further: the shipped binary is stripped, so the failing frame is not identifiable from outside.
What we ruled out (with evidence)
- Not the terminal emulator — occurred under both Ghostty and tmux.
- Not a pty wrapper — a wrapper was structurally absent from the process chain in both incidents (verified by process parentage, not assumed).
- Not an uninterruptible I/O hang — no process anywhere in
U/Dstate. - Not a blocking-write / pty deadlock — zero threads in
write(). - Not a CPU spin — CPU idle throughout.
- Not a dead session — the engine kept working (transcript + agents) for hours after the UI died.
Reproduction
We do not have a deterministic reproduction, and I won't pretend otherwise. What we have is a consistent correlation across both occurrences:
- Both freezes struck at a turn-end / Stop-hook boundary
- Both times an in-process background agent (Task tool) was active, and one had just reported completion/idle
- Both times the session had been working normally immediately before
Stated as correlation, not established cause. It may be a red herring.
Related but distinct issues
These look similar in the title but differ in signature — flagging so they aren't merged incorrectly:
- #23326 — TUI never accepts input, but 89–98% CPU spin; ours is idle.
- #20572 — unresponsive input, hypothesized blocked event loop / stalled SSE; in ours the event loop is demonstrably not blocked (engine keeps writing the transcript and completing agent work).
- #59750 — Windows Terminal, dead input loop (reported fixed in 2.1.15x); ours is macOS on 2.1.207.
- #22970 — freeze at startup; ours is mid-session after normal operation.
Questions / what would help
- Is there a known path where the TUI renderer or stdin listener is torn down without restarting — e.g. an unhandled exception in the render path triggered at turn-end when a background agent emits a completion/idle notification — that would leave the engine loop running?
- Is there a debug or verbose renderer flag we can enable so the next occurrence captures the failing path? We hit this regularly enough to catch it again, but the stripped binary gives us nothing.
- Would a
SIGWINCH-triggered renderer self-heal / re-attach be feasible as a mitigation, given the engine is still healthy and the session is fully recoverable via--resume?
Attachments
Process samples from both frozen instances and the WINCH/offset probe output are available on request (scrubbed). Session transcripts are not attached — they contain private conversation content.
3 Comments
Recurrence on 2.1.214 (2026-07-19, same macOS machine as the original report; Ghostty;
/tui fullscreen) — so the bug survived 2.1.207 → 2.1.214.Same signature as reported: stdout byte offset (fd 1,
lsof -o) frozen across 5 s and across aSIGWINCH(no repaint — a live TUI always repaints on WINCH); main thread idle inkevent64; zero threads in read/write; ~0% CPU. The engine stayed healthy — it finished the in-flight turn, ran all Stop hooks cleanly, and flushed the transcript.New data points from this occurrence:
stop_hook_summary+turn_durationevents were written to the transcript after the UI froze. So the death is in the render path while output is still streaming, not at the turn boundary — Stop hooks are downstream of the death, not a trigger.Addedt1pline,eremoved 1 line is— a merge of two renders of "Added 1 line, removed 1 line"), fragments of the statusline painted mid-screen, and stray inverse-video runs. It died mid-repaint.select(), not blocked) — consistent with the original finding that the terminal layer is irrelevant (previous occurrences: tmux and bare Ghostty, no wrapper).The final assistant message exists complete in the transcript but never painted on screen. Recovery unchanged: close the window,
claude --resume— transcript intact, nothing lost.Thanks for the unusually thorough diagnosis — your reading was right: this was an unhandled exception inside the terminal UI's render/layout path that left React's commit loop wedged, so the engine kept running (transcript, background agents, Stop hooks) while nothing ever painted again and keystrokes went nowhere. It also explains the torn final frame and the missing repaint on SIGWINCH.
This was fixed in 2.1.228: the layout pass is now guarded, retried from scratch on failure, and at worst drops a single frame instead of freezing the session (changelog: "Fixed interactive sessions that could stop redrawing entirely, while the process kept running, after a rare internal layout error" — https://github.com/anthropics/claude-code/blob/main/CHANGELOG.md). Your reports were on 2.1.207 and 2.1.214, both before the fix.
We couldn't reproduce it on 2.1.233 (it's rare and nondeterministic — roughly one machine per day fleet-wide), so I'm closing this as fixed. If you still see a dead-but-alive TUI on 2.1.228 or later, please reply with the version and
claude doctoroutput and we'll reopen — that would point at a different path.🤖 Generated with Claude Code
This issue has been automatically locked since it was closed and has not had any activity for 7 days. If you're experiencing a similar issue, please file a new issue and reference this one if it's relevant.