[BUG] Bracketed-paste block followed by a key in the same chunk drops the pasted text (breaks CJK IME input from mobile SSH clients like Termius)
Preflight Checklist
- [x] I have searched existing issues and this hasn't been reported yet (related but without a byte-level repro: #15705, #27857)
- [x] This is a single bug report
- [x] I am using the latest version of Claude Code (2.1.252)
What's Wrong?
When a bracketed-paste block is immediately followed by a plain key in the same stdin chunk, Claude Code drops the pasted text. The plain key (space) is kept.
ESC[200~녕ESC[201~<space> → "녕" is lost, only the space is inserted
This is exactly what mobile SSH clients produce for CJK IME input. Claude Code enables bracketed paste (ESC[?2004h); Termius (Android and iOS) then wraps every IME-committed Hangul syllable in paste markers. On a slow link the following space/enter is coalesced into the same TCP packet as the paste block, and the syllable before every space disappears. On a fast link the space arrives as a separate chunk and everything works — which is why the bug looks intermittent and "network dependent" to users.
Raw bytes captured from Termius 7.8.2 (Android, Samsung Keyboard) with the same terminal modes Claude Code enables (?1049h ?1000h ?1002h ?1003h ?1006h ?2004h ?1004h ?2031h):
7214ms 16B 1b 5b 32 30 30 7e eb 85 95 1b 5b 32 30 31 7e 20 '\x1b[200~녕\x1b[201~ ' ← syllable lost
8961ms 46B ... '\x1b[200~하\x1b[201~\x1b[200~세\x1b[201~\x1b[200~요\x1b[201~ ' ← all three lost
19674ms 15B 1b 5b 32 30 30 7e ec a7 80 1b 5b 32 30 31 7e '\x1b[200~지\x1b[201~'
19674ms 1B 20 ' ' ← separate chunk: works
Without bracketed paste enabled the same client sends plain 녕 and Claude Code handles it fine. Codex CLI and Gemini CLI in the identical setup receive the text correctly.
I believe #15705 (closed as not planned) and #27857 are the same bug seen from the user side; this report adds the exact byte sequence and a phone-free reproduction.
What Should Happen?
The paste block should be inserted and then the key processed, i.e. 안 + ESC[200~녕ESC[201~ should yield 안녕 in the input box.
Error Messages/Logs
# Results from the harness below, feeding bytes into a real `claude` (2.1.252) running in a pty,
# then forcing a redraw (SIGWINCH) and reading the prompt line.
[P1 [안] | [녕]+sp (one chunk)] -> '❯ 안' # 녕 dropped
[P2 [안] | [녕] | sp (separate)] -> '❯ 안녕' # OK
[P3 [하][세][요]+sp (one chunk)] -> '❯' # all dropped
[P4 replay of phone capture] -> '❯ 안반가' # 녕, 하세요, 워 dropped
# (spaces are not visible in the capture because Ink renders them as cursor moves; the syllables are what matters)
Steps to Reproduce
No phone needed. On Linux/macOS, in a trusted project directory:
# repro.py — run: python3 repro.py
import os, pty, select, time, re, signal, fcntl, termios, struct
ANSI = re.compile(rb'\x1b\[[0-9;?]*[ -/]*[@-~]|\x1b\][^\x07]*\x07|\x1b[PX^_].*?\x1b\\|\x1b[()][A-Za-z0-9]|\x1b[=>]')
BP = lambda t: b"\x1b[200~" + t.encode() + b"\x1b[201~"
def drain(fd, t):
end = time.time() + t; out = b""
while time.time() < end:
r, _, _ = select.select([fd], [], [], 0.05)
if r:
try: c = os.read(fd, 65536)
except OSError: break
if not c: break
out += c
return out
def run(name, steps, cols=60):
pid, fd = pty.fork()
if pid == 0:
os.environ["TERM"] = "xterm-256color"; os.environ.pop("CLAUDECODE", None)
os.execvp("claude", ["claude"])
fcntl.ioctl(fd, termios.TIOCSWINSZ, struct.pack("HHHH", 24, cols, 0, 0))
drain(fd, 7.0) # wait for the TUI
for data, gap in steps:
os.write(fd, data); drain(fd, gap)
# resize away and back -> Ink does a full redraw, so the prompt line is captured whole
fcntl.ioctl(fd, termios.TIOCSWINSZ, struct.pack("HHHH", 24, cols + 1, 0, 0)); os.kill(pid, signal.SIGWINCH); drain(fd, 0.5)
fcntl.ioctl(fd, termios.TIOCSWINSZ, struct.pack("HHHH", 24, cols, 0, 0)); os.kill(pid, signal.SIGWINCH)
txt = ANSI.sub(b"", drain(fd, 1.0)).decode("utf-8", "replace").replace("\r", "\n")
lines = [l.strip() for l in txt.split("\n") if "❯" in l]
print(f"[{name}] -> {lines[-1] if lines else '(no prompt line)'!r}")
os.write(fd, b"\x03"); time.sleep(0.3); os.write(fd, b"\x03"); time.sleep(0.4)
os.kill(pid, signal.SIGKILL); os.waitpid(pid, 0)
run("P1 paste+space in ONE chunk (bug)", [(BP("안"), 0.4), (BP("녕") + b" ", 0.3)])
run("P2 paste, then space in SEPARATE chunk (ok)", [(BP("안"), 0.4), (BP("녕"), 0.3), (b" ", 0.3)])
run("P3 three pastes + space in ONE chunk (bug)", [(BP("하") + BP("세") + BP("요") + b" ", 0.3)])
Expected: P1 → 안녕, P3 → 하세요. Actual: P1 → 안, P3 → empty. (Nothing is submitted; no API calls are made.)
Manual reproduction: SSH from Termius on Android/iOS with a Korean keyboard, type any Hangul word followed by a space over a non-LAN connection.
Claude Model
Not sure / Multiple models
Is this a regression?
I don't know
Last Working Version
_No response_
Claude Code Version
2.1.252 (Claude Code)
Platform
Anthropic API
Operating System
Ubuntu/Debian Linux
Terminal/Shell
Other
Additional Information
- Server: Ubuntu, kernel 6.8,
TERM=xterm-256color,LANG=en_US.UTF-8; client: Termius 7.8.2 on Samsung Galaxy S23 (Samsung Keyboard). Termius identifies itself asESC P>|Termius 7.8.2 ESC \/ DA1ESC[?1;2c. - Workaround that fully fixes it for me: a small pty wrapper between the SSH session and
claudethat rewrites short single-line paste blocks (ESC[200~X ESC[201~, X ≤ 48 bytes, no newline) into plainXbefore forwarding. With that in place P1/P3/P4 all come through intact, and real multi-line pastes still work because their markers are left alone. - Suggested fix on Claude Code's side: when parsing an input chunk, flush the pasted text when
ESC[201~is seen and continue parsing the remainder of the chunk as normal key input, instead of discarding the paste when other bytes follow the end marker in the same chunk.