[BUG] Shifted keys lost in WezTerm since 2.1.247: Kitty "report alternate keys" flag is requested but the shifted-key field is never parsed

Status Open
Reported on v2.1.247
Maintainer reply None cached
Activity 0 comments · opened Sep 4, 2026

Preflight Checklist

  • [x] I have searched existing issues and this hasn't been reported yet
  • [x] This is a single bug report (please file separate reports for different bugs)
  • [x] I am using the latest version of Claude Code

What's Wrong?

Since 2.1.247 Claude Code enables the Kitty keyboard protocol with flag 4 ("report alternate keys") by sending ESC[>5u on startup; 2.1.246 sent ESC[>1u. With flag 4 set, WezTerm (20240203 stable) stops sending shifted printable keys as plain text and encodes them as CSI u with the unshifted codepoint plus the shifted key in the alternate field, e.g. Shift+1 becomes ESC[49:33;2u. Claude Code's CSI u parser reads the first sub-parameter (unshifted key) and the third (base layout, used for Ctrl) but never the second (shifted key), and reconstructs the character by upper-casing the unshifted key. That works for letters and is wrong for every symbol: ESC[49:33;2u inserts 1, ESC[59:58;2u inserts ;, ESC[47:63;2u inserts /.

In my setup (WezTerm on Windows 11 hosting a WSL2 Ubuntu domain) the effect is worse: Shift+A, Shift+1 and Shift+? insert nothing at all, while Shift+Tab and Shift+Enter still work. Those two are the only shifted keys WezTerm encodes without a colon-separated sub-parameter, so the colon-bearing sequences appear to be discarded on the WezTerm to ConPTY to wsl.exe input path before they reach Claude Code (inferred from the symptom pattern, not traced in ConPTY). Either way the trigger is the same: requesting flag 4 without consuming the field it adds.

A secondary parser gap: any CSI u whose modifier carries an event-type sub-field (ESC[97:65;2:1u) is dropped entirely, because the regex ^\x1b\[(\d+)(?::(\d*)(?::(\d+))?)?(?:;(\d+))?u only accepts a bare modifier number.

Note kitty itself is unaffected: per the protocol it keeps sending plain text for shifted text keys unless flag 8 is set. WezTerm deviates by escaping them once flag 4 is present, but a CSI u carrying a shifted-key field is valid protocol input that the application is expected to honour.

What Should Happen?

When a CSI u key report carries a shifted-key sub-field and Shift is the only modifier, the inserted text should be the shifted key (!, :, ?, A), as described in the Kitty keyboard protocol's "report alternate keys" section. Keybinding matching can keep using the unshifted key. Modifier fields with an event-type sub-field (;2:1u) should be parsed rather than dropped, even if release and repeat events are then ignored.

Suggested fix: in the CSI u handler use String.fromCodePoint(shifted) for text insertion when the second sub-parameter is present, and widen the modifier group in the regex to (\d+)(?::(\d+))?. Alternatively only request flag 4 when a non-Latin layout is detected, but the parser fix is what makes flag 4 safe.

Error Messages/Logs (auto-formatted as code, so plain text)
Startup sequences sent to the terminal (captured in a pty, TERM_PROGRAM=WezTerm):
2.1.246: ESC[<u ESC[>1u ESC[>4;2m
2.1.247+: ESC[<u ESC[>5u ESC[>4;2m (checked 2.1.247, 2.1.258, 2.1.259, 2.1.260)

What WezTerm 20240203 emits with flags 5 (from KeyEvent::encode_kitty in
When a CSI u key report carries a shifted-key sub-field and Shift is the only modifier, the inserted text should be the shifted key (!, :, ?, A), as described in the Kitty keyboard protocol's "report alternate keys" section. Keybinding matching can keep using the unshifted key. Modifier fields with an event-type sub-field (;2:1u) should be parsed rather than dropped, even if release and repeat events are then ignored.

Suggested fix: in the CSI u handler use String.fromCodePoint(shifted) for text insertion when the second sub-parameter is present, and widen the modifier group in the regex to (\d+)(?::(\d+))?. Alternatively only request flag 4 when a non-Latin layout is detected, but the parser fix is what makes flag 4 safe.

Error Messages/Logs (auto-formatted as code, so plain text)
Startup sequences sent to the terminal (captured in a pty, TERM_PROGRAM=WezTerm):
2.1.246: ESC[<u ESC[>1u ESC[>4;2m
2.1.247+: ESC[<u ESC[>5u ESC[>4;2m (checked 2.1.247, 2.1.258, 2.1.259, 2.1.260)

What WezTerm 20240203 emits with flags 5 (from KeyEvent::encode_kitty in wezterm-input-types/src/lib.rs at that tag; the legacy text path requires !REPORT_ALTERNATE_KEYS):
Shift+A ESC[97:65;2u
Shift+1 ESC[49:33;2u
Shift+; ESC[59:58;2u
Shift+/ ESC[47:63;2u
Shift+Tab ESC[9;2u
Shift+Enter ESC[13;2u

What Claude Code 2.1.260 inserts when fed those bytes in a pty (same result in fullscreen and classic renderer):
input expected actual
ESC[97:65;2u A A
ESC[49:33;2u ! 1
ESC[59:58;2u : ;
ESC[47:63;2u ? /
ESC[97:65;2:1u A (dropped)
ESC[97;2:1u A (dropped)
ESC[97:65;130u A A (Num Lock bit handled fine)
ESC[9;2u mode cycles ok
ESC[13;2u newline ok

Error Messages/Logs

Steps to Reproduce

  1. WezTerm 20240203-110809-5046fc22 with enable_kitty_keyboard = true; Claude Code 2.1.247 or later.
  2. Run claude, focus the prompt, press Shift+1 or Shift+?.
  3. Observe: nothing inserted (WezTerm on Windows hosting WSL2), or the unshifted 1 / / inserted (any platform, from the parser behaviour above). Shift+Tab still cycles modes.
  4. Downgrade to 2.1.246 or set enable_kitty_keyboard = false: shifted keys work again.

Terminal-independent reproduction: spawn claude inside a pty with TERM_PROGRAM=WezTerm, wait for the prompt to render, write the bytes \x1b[49:33;2u to its stdin. 1 appears in the prompt instead of !.

Claude Model: not model related; pick what you normally run, or "None" if offered.
Is this a regression?: Yes, this worked in a previous version.
Last Working Version: 2.1.246
Claude Code Version: 2.1.260 (Claude Code)
Platform: this asks for the API platform, so pick the one matching your login.
Operating System: "WSL" if offered, otherwise "Linux". The host is noted below.
Terminal/Shell: "WezTerm" if offered, otherwise "Other".

Claude Model

Other

Is this a regression?

Yes, this worked in a previous version

Last Working Version

2.1.246

Claude Code Version

2.1.260

Platform

Anthropic API

Operating System

Ubuntu/Debian Linux

Terminal/Shell

Other

Additional Information

Environment detail: WezTerm 20240203-110809-5046fc22 (current stable) running on Windows 11 with default_domain = 'WSL:Ubuntu'; Claude Code is the native Linux x64 build inside WSL2 (kernel 6.6.87.2-microsoft-standard-WSL2), TERM=xterm-256color, TERM_PROGRAM=WezTerm, zsh. Reproduced with "tui": "fullscreen" and with the classic renderer.

How the regression version was pinned down: session transcripts showed 2.1.246 as the daily version until Sep 3; the shipped Linux binaries for 2.1.246 and 2.1.247 were compared and the Kitty push string changes from >1u to >5u in 2.1.247 (a >1u variant remains for the "legacyKitty" background-session attach path). This lines up with the 2.1.247 changelog entry "Fixed Ctrl keyboard shortcuts not firing under non-Latin (e.g. Cyrillic) keyboard layouts in kitty-protocol terminals", which presumably needs the base-layout sub-field that flag 4 provides.

Workaround that restores everything in WezTerm:

config.enable_kitty_keyboard = false
config.keys = {
  { key = 'Enter', mods = 'SHIFT', action = act.SendString('\x1b\r') },  -- Shift+Enter newline; ESC[27;2;13~ also dies on the ConPTY path
  ...
}
Alternative: launch with TERM_PROGRAM set to anything other than WezTerm so Claude Code never requests Kitty mode (Shift+Enter then submits; Ctrl+J still inserts a newline).

Protocol reference: https://sw.kovidgoyal.net/kitty/keyboard-protocol/#report-alternate-keys
WezTerm encoder: https://github.com/wez/wezterm/blob/20240203-110809-5046fc22/wezterm-input-types/src/lib.rs (KeyEvent::encode_kitty, use_legacy condition)

View original on GitHub ↗