Terminal identification queries are sent during a ~50ms window where ECHO is re-enabled, leaking replies to the screen at startup
Summary
On startup Claude Code briefly restores the tty to cooked mode (ECHO on) and, inside that
window, emits its terminal identification queries CSI > 0 q (XTVERSION) and CSI c (Primary DA).
Any terminal that answers quickly gets its replies echoed back by the kernel line discipline in
caret notation, printing garbage above the banner.
The same queries are re-sent ~110ms later with ECHO correctly off, so the probe itself works —
only the first, premature emission leaks.
Environment
- Claude Code 2.1.261 (also present in 2.1.260; appeared in the 2.1.251 → 2.1.259 range)
- Arch Linux (CachyOS), kernel 7.2.2
- xfce4-terminal 1.2.0 / VTE 0.84.1
TERM=xterm-256color,COLORTERM=truecolor
What the user sees
$ claude
^[[I^[P>|VTClaude)Code[v2.1.2601;22;28c ▐▛███▛█
▝▜██████▀ Opus 5 with high effort · Claude Pro
▝▝ ▝▝ ~/.claude
Decoded, three terminal replies interleaved with the banner text:
| Fragment | Origin |
|---|---|
| ^[[I | focus-in report, from mode 1004 being briefly enabled |
| ^[P>\|VTE(…) | XTVERSION reply to CSI > 0 q |
| …1;22;28c | Primary DA reply to CSI c |
Evidence
Captured by running claude on a pty, logging every byte it writes with timestamps while
sampling the slave tty's ECHO flag:
[ 0.000] ECHO -> on
[ 0.040] ECHO -> off <- raw mode set correctly
[ 0.183] OUT ^[7^[[r^[8^[[?25h
[ 0.200] OUT ^[[?25l
[ 0.201] OUT ^[[?2004h^[[?2031h^[[?1004h <- bracketed paste, colour-notify, focus reporting ON
[ 0.202] OUT ^[[>4m^[[<u^[[?1004l^[[?2031l^[[?2004l <- immediately torn back down
[ 0.202] ECHO -> on <- BUG: cooked mode restored
[ 0.206] OUT ^[[>0q^[[c <- BUG: XTVERSION + DA sent with ECHO on
[ 0.253] OUT ^[[?2004h^[[?2031h^[[?1004h
[ 0.253] ECHO -> off
[ 0.260] OUT ^[]0;… Claude Code^G
[ 0.280] OUT <banner>
[ 0.317] OUT ^[[>0q^[[c <- same queries, correctly sent with ECHO off
The queries at 0.206 sit inside the 51ms cooked-mode window opened at 0.202. xfce4-terminal
answers in about a millisecond, well within it.
Reproduction
Script used to capture the above (no prompt is sent, so no API request):probe-startup-escapes.py — spawns claude on a pty, records output bytes and ECHO
transitions for N seconds, renders control characters in caret notation.
Expected
Terminal identification queries are only emitted while the tty is in raw mode with ECHO
disabled, so replies are consumed by the parser rather than echoed by the line discipline.
Notes
- Not terminal-specific in mechanism: any terminal answering XTVERSION or DA within ~50ms
will leak. Terminals without XTVERSION support lose only the ^[P fragment.
- Enabling and immediately disabling mode 1004 at
0.201/0.202is also what produces the
stray ^[[I focus report.
- No environment variable gates this;
CLAUDE_CODE_DISABLE_TERMINAL_TITLEonly suppresses the
OSC 0 title write at 0.260.
- Related but distinct: #21766, #10375.
Showing cached comments. Read the full discussion on GitHub ↗
4 Comments
Follow-up with measured reply latencies, since the original report estimated them rather than measuring.
Terminal: xfce4-terminal 1.2.0 / VTE 0.84.1,
TERM=xterm-256color, local (no ssh, no tmux).15 samples per query, time to first byte of the reply:
| Query | min (ms) | median (ms) | max (ms) | inside the 47ms window |
|---|---|---|---|---|
|
CSI c(Primary DA) | 0.10 | 0.41 | 14.49 | 15/15 ||
CSI > 0 q(XTVERSION) | 0.09 | 0.39 | 13.62 | 15/15 ||
CSI ? 1004 h(focus report) | 0.15 | 0.46 | 17.41 | 15/15 |Exact replies from this terminal:
CSI c->^[[?61;1;21;22;28cCSI > 0 q->^[P>|VTE(8401)^[\CSI ? 1004 h->^[[IThose three account for exactly the three fragments in the banner garbage in the original report.
Median margin is ~115x -- the reply is back long before
ECHOis disabled again at t=0.253. Even the worst outlier is 2.7x inside the window.Implication for triage: a terminal escapes this only by being slower than ~47ms. Terminals that parse synchronously will leak more deterministically than VTE, which is among the lazier ones about answering. The only configuration that reliably does not leak is ssh with >47ms RTT, since the line discipline doing the echoing is the remote pty's.
(Secondary DA
CSI > calso answers in 0.31ms median here, but Claude Code does not send it -- noted only for completeness.)Confirming on a different platform, plus an exit-side variant of the same leak.
Environment
TERM=xterm-256colorStartup — same as reported. ConPTY answers DA1 itself (no round-trip to the terminal app), so the reply lands well inside the cooked-mode window: in my capture ECHO goes back on at t=0.739s,
CSI cis sent at 0.747s, reply echoed once. The reply here is^[[?61;4;6;7;14;21;22;23;24;28;32;42;52c. It flashes and is overpainted by the first render.Exit — not covered above: the teardown emits the same two queries again, right before restoring the tty and exiting. The reply arrives after the process is gone, ECHO is on, and the line discipline paints it onto the shell prompt:
Teardown byte stream captured with
scripton a pty (Ctrl+D):The queries go out first, the mode teardown follows immediately, and nothing waits for the answer. Same family as #89376 (subcommands), but this is the interactive session.
Bisect (escape-only skeleton of startup+teardown on the same pty; 2.1.260/261/263 are byte-identical to each other):
CSI > 0 q+CSI c~45 bytes later → no query at teardown. No leak.Workaround covering both ends: launch with ECHO already off. Claude Code restores the termios it saved at startup, so the cooked-mode window stays silent; after exit, drain stdin before re-enabling echo (otherwise the late reply is echoed right then). Verified on a pty: 0 echoed replies (vs 1), typed input still rendered by the app, tty restored, exit code preserved.
Same bug, one more data point from macOS inside tmux, where the timing makes it show up only for large
--resumesessions.Environment
TERM_PROGRAM=ghostty), zsh 5.9TERM=xterm-ghosttyinside tmuxWhat I see
Resuming a large session inside tmux prints this above the UI:
That is tmux answering XTVERSION (
ESC P > | tmux ... ESC \) and Primary DA (ESC [ ? 1 ; 2 c), echoed by the line discipline.What narrows it down
claudewith no--resumein the same pane: no leak.So on this setup the cooked-mode window is only long enough to catch tmux's reply when the transcript load is slow. That matches the ~50 ms window in the trace above: a local terminal answers inside it, tmux does not, unless the window stretches while a big transcript loads. Looks like the same fix covers both: send the probe only after raw mode is set, or drain the replies before restoring cooked mode. #93004 reports the same sequence on Ghostty without tmux.
Retested on 2.1.267 (current release). The startup half is still there, and I can put a number on @pszypowicz's
--resumeobservation: the cooked-mode window is bounded by the transcript load, so it scales with session size.Method —
claudespawned on a synthetic pty; the harness plays the terminal, answering XTVERSION and Primary DA immediately (0 ms round-trip, i.e. worst case) while samplingECHOon the pty pair. WSL2 / Ubuntu 24.04 / kernel 6.18.33,TERM=xterm-256color, no prompt ever submitted.Startup — still leaks on 2.1.267
Immediately before the window opens, the modes are torn down (
CSI >4m · CSI <u · CSI ?1004l · CSI ?2031l · CSI ?2004l) — same shape as the original trace. Window width on a fresh start, 7 runs: 114, 117, 135, 141, 142, 142, 152 ms (median 141).The
--resumeeffect, measuredSame binary, same pane, resuming a 40 MB transcript:
| run | cooked-mode window |
|---|---|
| fresh × 7 | 114–152 ms |
|
--resume40 MB × 2 | 1056 ms, 1548 ms |So the window is not a fixed ~50 ms — it stays open for as long as the transcript load takes, ~10x wider here. That is the mechanism @pszypowicz inferred, confirmed by measurement: tmux is too slow to answer inside the short window, but a load of that length stretches it far enough to catch tmux's reply. Anything that slows startup (large transcript, cold page cache, slow MCP init) widens the exposure the same way.
Teardown — fixed
The exit-side variant I reported above (queries re-sent at teardown, reply painted onto the shell prompt after exit) is gone in 2.1.267: no XTVERSION or DA anywhere in the teardown byte stream, 3/3 runs exiting with Ctrl+D. Only the startup half remains, so the
read -rsdrain in that workaround is no longer needed —stty -echobefore launch still covers what is left.Triage note — the window only opens once the session actually starts. In a directory that has not been trusted yet, the run stops at the trust prompt, the second window never opens, and there is no leak. "Cannot reproduce in a scratch directory" is expected and does not mean it is fixed.