Terminal identification queries are sent during a ~50ms window where ECHO is re-enabled, leaking replies to the screen at startup

Status Open
Reported on v2.1.261
Maintainer reply None cached
Activity 6 comments · opened Sep 5, 2026

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.202 is also what produces the

stray ^[[I focus report.

  • No environment variable gates this; CLAUDE_CODE_DISABLE_TERMINAL_TITLE only suppresses the

OSC 0 title write at 0.260.

  • Related but distinct: #21766, #10375.

View original on GitHub ↗

4 Comments

ansulev · 1 month ago

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;28c
  • CSI > 0 q -> ^[P>|VTE(8401)^[\
  • CSI ? 1004 h -> ^[[I

Those 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 ECHO is 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 > c also answers in 0.31ms median here, but Claude Code does not send it -- noted only for completeness.)

chcarva · 1 month ago

Confirming on a different platform, plus an exit-side variant of the same leak.

Environment

  • Claude Code 2.1.263 (also 2.1.260, 2.1.261). 2.1.259 does not have it — bisect below
  • WSL2 2.7.8 / Ubuntu 24.04.4 / kernel 6.18.33 / bash 5.2.21
  • Windows Terminal 1.24.11911 (ConPTY), TERM=xterm-256color

Startup — 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 c is 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:

$ claude
^[[?61;4;6;7;14;21;22;23;24;28;32;42;52c$

Teardown byte stream captured with script on a pty (Ctrl+D):

CSI ?2026 l · CSI > 0 q · CSI c · CSI ?1006 l … CSI ?2004 l · CSI ?25 h · ESC 7 · CSI r · ESC 8 · OSC 0 ; BEL

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):

  • 2.1.259: raw on → CSI > 0 q + CSI c ~45 bytes later → no query at teardown. No leak.
  • 2.1.260+: raw on → raw off → queries → raw on at startup, and queries again at teardown. Introduced in 2.1.260.

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.

cl() {
  [ -t 0 ] && [ -t 1 ] || { claude "$@"; return; }
  local st rc junk; st=$(stty -g)
  trap 'stty "$st" 2>/dev/null; trap - INT RETURN' INT RETURN
  stty -echo
  claude "$@"; rc=$?
  read -rs -t 0.1 -N 4096 junk 2>/dev/null   # still with ECHO off
  stty "$st"; trap - INT RETURN
  return $rc
}
pszypowicz · 1 month ago

Same bug, one more data point from macOS inside tmux, where the timing makes it show up only for large --resume sessions.

Environment

  • Claude Code 2.1.261
  • macOS 26.6.2, Ghostty (TERM_PROGRAM=ghostty), zsh 5.9
  • tmux 3.8 development build, TERM=xterm-ghostty inside tmux

What I see

Resuming a large session inside tmux prints this above the UI:

^[P>|tmux next-3.8^[\^[[?1;2c

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

  • Same terminal, same tmux server, resuming a small session: no leak.
  • Resuming a large session (the run takes about 20 seconds before the UI appears): leak every time.
  • A fresh claude with no --resume in 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.

chcarva · 1 month ago

Retested on 2.1.267 (current release). The startup half is still there, and I can put a number on @pszypowicz's --resume observation: the cooked-mode window is bounded by the transcript load, so it scales with session size.

Method — claude spawned on a synthetic pty; the harness plays the terminal, answering XTVERSION and Primary DA immediately (0 ms round-trip, i.e. worst case) while sampling ECHO on 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

[0.447] ECHO -> on                                <- cooked mode restored
[0.453] OUT  ^[[>0q^[[c                           <- XTVERSION + DA sent with ECHO on
[0.453] OUT  ^[P>|Probe(1)^[\^[[?61;1;22;28c      <- our replies, echoed back
[0.589] ECHO -> off
[0.854] OUT  ^[[>0q^[[c                           <- same queries, correctly, ECHO off

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 --resume effect, measured

Same binary, same pane, resuming a 40 MB transcript:

| run | cooked-mode window |
|---|---|
| fresh × 7 | 114–152 ms |
| --resume 40 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 -rs drain in that workaround is no longer needed — stty -echo before 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.

Showing cached comments. Read the full discussion on GitHub ↗