[BUG] Vi mode escape key has 50ms hardcoded delay in tmux — kitty keyboard protocol negotiation silently swallowed
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?
When using vim keybindings inside tmux, pressing Escape to exit insert mode incurs a 50ms delay (NORMAL_TIMEOUT=50 in Claude Code's modified Ink input layer) before the key is recognized. This makes rapid esc + motion key chaining (e.g., esc then b to move back a word) noticeably slower than outside tmux, where the kitty keyboard protocol encodes Escape as the unambiguous \033[27u sequence that is parsed instantly with 0ms delay.
The root cause is that tmux always sends bare \033 for unmodified Escape to inner programs, regardless of extended-keys or extended-keys-format settings. Claude Code's tokenizer receives bare \033, marks the parse state as "incomplete" (it could be the start of an escape sequence), and waits NORMAL_TIMEOUT (50ms) before flushing it as a standalone Escape.
This 50ms penalty occurs regardless of how Claude Code detects the terminal:
Case 1 — default-terminal "xterm-ghostty": Claude Code's U_D() terminal detection function matches process.env.TERM === "xterm-ghostty" and returns "ghostty", which is in the sB_ allow-list ["iTerm.app","kitty","WezTerm","ghostty"]. Claude Code enables the kitty keyboard protocol by writing \033[>1u to stdout. However, tmux has no handler for this CSI sequence — its input_csi_table only maps { 'u', "", INPUT_CSI_RCP } (no > intermediate). The enable sequence is silently dropped. Ghostty never enters kitty keyboard mode for this pane. tmux sends bare \033 for Escape. Claude Code waits 50ms.
Case 2 — default-terminal "tmux-256color": Claude Code's U_D() falls through to process.env.TMUX and returns "tmux", which is NOT in the sB_ allow-list. Kitty protocol is never enabled. tmux sends bare \033 for Escape. Claude Code waits 50ms.
Why this doesn't happen outside tmux: When Claude Code runs directly in Ghostty (or iTerm2, kitty, WezTerm), the terminal receives \033[>1u, enters kitty keyboard mode, and encodes Escape as \033[27u. Claude Code's tokenizer matches it against the CSI u regex ^\x1b\[(\d+)(?:;(\d+))?u, resolves codepoint 27 as "escape", and emits the key immediately with 0ms delay.
Why other vim-mode apps in tmux don't have this lag: zsh vi-mode has a configurable KEYTIMEOUT (typically 1 = 10ms). Neovim has a configurable ttimeoutlen. Claude Code's NORMAL_TIMEOUT=50 is hardcoded and not configurable via any setting, environment variable, or config file.
What Should Happen?
Escape should be recognized with minimal delay inside tmux, comparable to the experience outside tmux. Possible fixes:
- Make
NORMAL_TIMEOUTconfigurable via an environment variable (e.g.,CLAUDE_CODE_ESCAPE_TIMEOUT=10). This is the simplest fix and lets users choose their own trade-off. - Reduce the default
NORMAL_TIMEOUTfrom 50ms to 10-15ms. Modern terminals and tmux withescape-time 0deliver complete escape sequences well within 10ms. - Detect tmux and use DCS passthrough to negotiate kitty keyboard protocol with the outer terminal, similar to how Neovim handles this.
Error Messages/Logs
# No error messages. The issue is a 50ms input latency, not a crash.
# Relevant code extracted from the compiled binary (~/.local/share/claude/versions/2.1.59):
# Hardcoded timeout in modified Ink InternalApp class:
NORMAL_TIMEOUT = 50; # 50ms escape disambiguation
PASTE_TIMEOUT = 500;
# Terminal allow-list for kitty keyboard protocol:
sB_ = ["iTerm.app", "kitty", "WezTerm", "ghostty"];
# Terminal detection — TERM=xterm-ghostty triggers "ghostty" even inside tmux:
if (process.env.TERM === "xterm-ghostty") return "ghostty";
# Kitty protocol enable sequence (swallowed by tmux):
i9_ = $G(">1u") # resolves to \033[>1u
# Input processing with escape timeout:
processInput = (T) => {
let [R, A] = dB_(this.keyParseState, T);
if (this.keyParseState.incomplete) {
this.incompleteEscapeTimer = setTimeout(
this.flushIncomplete,
this.keyParseState.mode === "IN_PASTE"
? this.PASTE_TIMEOUT # 500ms
: this.NORMAL_TIMEOUT # 50ms <-- the delay
);
}
};
Steps to Reproduce
- Configure tmux:
set -s escape-time 0,set -s extended-keys always,set -s extended-keys-format csi-u - Launch Claude Code inside a tmux pane (any terminal emulator — Ghostty, iTerm2, kitty, etc.)
- Type some text, then rapidly press
Escapefollowed byb(within ~40ms) - Expected: Exits insert mode and moves cursor back one word
- Actual:
esc+bis interpreted as Alt+b or the motion doesn't register because Escape hasn't been recognized yet - Compare with running Claude Code directly in the terminal (no tmux) — the same rapid
esc+bchain works flawlessly
Claude Model
Not sure / Multiple models
Is this a regression?
No, this never worked (probably? It's never worked for me)
Last Working Version
N/A
Claude Code Version
2.1.59
Platform
Anthropic API
Operating System
macOS
Terminal/Shell
Ghostty + tmux
Additional Information
Environment details:
- macOS Darwin 25.2.0 (arm64)
- Ghostty 1.2.3
- tmux 3.6a
xterm-ghosttyterminfo installed (ncurses 6.5+)
tmux config (relevant settings):
set -s escape-time 0
set -s default-terminal "xterm-ghostty"
set -s extended-keys always
set -s extended-keys-format csi-u
Why tmux can't send \033[27u for bare Escape:
- tmux's
input_key()ininput-keys.chas a "trivial case" fast path that sends bare\033forC0_ESCbefore reaching the extended key encoding logic - Even if that fast path were bypassed,
input_key_extended()rejects unmodified keys — the modifier switch hasdefault: return (-1), so keys without Shift/Ctrl/Meta are not CSI u encoded - tmux's
tty_keys_extended_key()input parser requires%u;%uformat (key + modifier), so bare\033[27u(no modifier) from the outer terminal would also fail to parse
Related issues: #6072, #16738 (auto-closed, never fixed), #27868
This issue has 4 comments on GitHub. Read the full discussion on GitHub ↗