[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.