# The Keybinding System Fully Explained

This chapter analyzes Claude Code's shortcut management system — how it achieves context-aware keybinding distribution, chord combinations, platform adaptation, and user-defined hot reloading across 18 different UI contexts.

> ⚠️ **Feature Gate Reminder**: Keybinding customization is currently available only to Anthropic employees (controlled via a GrowthBook feature gate). External users always use the default bindings and cannot customize them. The analysis in this chapter of user customization, hot reloading, and unbinding mechanisms describes the system's complete design, but please note that these capabilities are not yet available to the vast majority of users.

## Overview

Claude Code implements a complete keybinding system supporting context awareness, chord bindings, platform adaptation, user customization, and hot reloading. This system manages everything from `ctrl+c` to `ctrl+k ctrl+s`, routing user input precisely across 18 different UI contexts.

**Technical Analogy (OS Perspective)**: The keybinding system is like the operating system's **shortcut manager** — the Windows hotkey registry or macOS Keyboard Shortcuts settings panel. Different application windows (contexts) can register different behaviors for the same shortcut, and the system is responsible for distributing key events based on the current focus window and handling registration conflicts.

> 💡 **Plain English**: Keybindings are like a regular's shorthand order at a restaurant — saying "the usual" (shortcut) gets the dish served directly, while a new customer browses the menu (command palette). And different seating areas (contexts) have different shortcuts: at the bar, "a drink" defaults to beer (Enter = submit in Chat context), while in a private room, "a drink" defaults to tea (Enter = confirm in Confirmation context).

### 🌍 Industry Context

The keybinding system is foundational infrastructure for all interactive developer tools. Implementation strategies vary widely across tools:

- **VS Code**: Has the industry's most mature keybinding architecture — supporting `when` conditional expressions (e.g., `editorTextFocus && !editorReadonly`), multi-layer overrides (default/extension/user/workspace), chord bindings, and a complete GUI keybinding editor. This is the target Claude Code explicitly references but has not yet fully caught up to.
- **Cursor / Windsurf**: As VS Code forks, they directly inherit VS Code's entire keybinding framework, including the `keybindings.json` format and `when` condition system. They build on this by adding AI-specific shortcuts (e.g., Cursor's `Cmd+K` inline editing).
- **GitHub Copilot**: Runs as a VS Code extension, registering shortcuts via `contributes.keybindings`, constrained by the host editor's keybinding framework.
- **Aider**: A pure terminal tool with very simple keybindings — relying on readline defaults, with no user customization or chord combinations.
- **CodeX (OpenAI)**: The underlying layer has been rewritten in Rust (95.6%), with hardcoded fixed keybinding collections and no customization capability. **OpenCode**, as a TUI tool implemented in Go+Zig, switches Build/Plan mode via the Tab key, with interaction design closer to traditional terminal applications.

Claude Code's keybinding system sits in a unique position between Aider (minimalist) and VS Code (full-featured): it implements chord bindings, context distribution, and hot reloading in a terminal environment — features usually found only in GUI editors — while being constrained by the terminal's modifier key recognition capabilities.

---

## 1. Configuration Format and Schema

### 1.1 JSON Schema Definition

The keybinding configuration file (`~/.claude/keybindings.json`) uses a strict Zod-defined schema:

```typescript
// Source: src/keybindings/schema.ts (lines 177-229)
export const KeybindingsSchema = lazySchema(() =>
  z.object({
    $schema: z.string().optional(),
    $docs: z.string().optional(),
    bindings: z.array(KeybindingBlockSchema()).describe('Array of keybinding blocks by context'),
  })
)
```

The top-level structure of the configuration file is an object containing a `bindings` array. Each block specifies a context and the binding mappings within that context:

```json
{
  "bindings": [
    {
      "context": "Chat",
      "bindings": {
        "ctrl+s": "chat:stash",
        "ctrl+k ctrl+e": "chat:externalEditor"
      }
    }
  ]
}
```

### 1.2 The 18 Contexts

The system defines 18 UI contexts, each representing an interface state that can receive keyboard input:

```typescript
// Source: src/keybindings/schema.ts (lines 12-32)
export const KEYBINDING_CONTEXTS = [
  'Global',          // Global — effective regardless of focus
  'Chat',            // Chat input box focused
  'Autocomplete',    // Autocomplete menu visible
  'Confirmation',    // Confirmation / permission dialog
  'Help',            // Help overlay
  'Transcript',      // Viewing conversation transcript
  'HistorySearch',   // History search (ctrl+r)
  'Task',            // Background task running
  'ThemePicker',     // Theme picker
  'Settings',        // Settings menu
  'Tabs',            // Tab navigation
  'Attachments',     // Image attachment navigation
  'Footer',          // Footer indicator
  'MessageSelector', // Message selector (backtracking)
  'DiffDialog',      // Diff dialog
  'ModelPicker',     // Model picker
  'Select',          // Generic selection list
  'Plugin',          // Plugin dialog
] as const
```

### 1.3 Action Enumerations

The system predefines 70+ valid action identifiers:

```typescript
// Source: src/keybindings/schema.ts (lines 64-172)
export const KEYBINDING_ACTIONS = [
  // App level
  'app:interrupt', 'app:exit', 'app:toggleTodos', 'app:toggleTranscript',
  // Chat level
  'chat:cancel', 'chat:submit', 'chat:newline', 'chat:undo',
  // Confirmation dialog
  'confirm:yes', 'confirm:no', 'confirm:toggle',
  // More...
  'voice:pushToTalk',
] as const
```

It also supports `command:xxx` format command bindings, which can directly trigger slash commands (e.g., `command:help`), but these must be used within the Chat context.

---

## 2. Parser

### 2.1 Keystroke String Parsing

The parser converts human-readable keystroke strings (e.g., `ctrl+shift+k`) into structured `ParsedKeystroke` objects:

```typescript
// Source: src/keybindings/parser.ts (lines 13-75)
export function parseKeystroke(input: string): ParsedKeystroke {
  const parts = input.split('+')
  const keystroke: ParsedKeystroke = {
    key: '', ctrl: false, alt: false, shift: false, meta: false, super: false,
  }
  for (const part of parts) {
    const lower = part.toLowerCase()
    switch (lower) {
      case 'ctrl': case 'control': keystroke.ctrl = true; break
      case 'alt': case 'opt': case 'option': keystroke.alt = true; break
      case 'shift': keystroke.shift = true; break
      case 'meta': keystroke.meta = true; break
      case 'cmd': case 'command': case 'super': case 'win': keystroke.super = true; break
      case 'esc': keystroke.key = 'escape'; break
      case 'return': keystroke.key = 'enter'; break
      case 'space': keystroke.key = ' '; break
      default: keystroke.key = lower; break
    }
  }
  return keystroke
}
```

It supports rich modifier aliases: `ctrl`/`control`, `alt`/`opt`/`option`/`meta`, `cmd`/`command`/`super`/`win`. This ensures cross-platform and cross-user-habit compatibility.

### 2.2 Chord Parsing

Chord bindings are whitespace-separated sequences of multiple keystrokes:

```typescript
// Source: src/keybindings/parser.ts (lines 80-84)
export function parseChord(input: string): Chord {
  if (input === ' ') return [parseKeystroke('space')]  // Special handling: space key itself
  return input.trim().split(/\s+/).map(parseKeystroke)
}
```

`ctrl+k ctrl+s` is parsed as an array of two `ParsedKeystroke` objects. Note the special case: a single space character `' '` is recognized as a space key binding rather than a separator — this is an easy-to-miss edge case.

### 2.3 Platform-Aware Display

When displaying, modifier names are chosen based on the platform:

```typescript
// Source: src/keybindings/parser.ts (lines 157-176)
export function keystrokeToDisplayString(ks, platform = 'linux'): string {
  const parts: string[] = []
  if (ks.ctrl) parts.push('ctrl')
  if (ks.alt || ks.meta) {
    parts.push(platform === 'macos' ? 'opt' : 'alt')  // macOS uses opt
  }
  if (ks.shift) parts.push('shift')
  if (ks.super) {
    parts.push(platform === 'macos' ? 'cmd' : 'super')  // macOS uses cmd
  }
  parts.push(displayKey)
  return parts.join('+')
}
```

Alt and meta are merged at the display level: macOS shows `opt`, other platforms show `alt`. This reflects a terminal limitation — most terminals cannot distinguish Alt from Meta.

---

## 3. Matching Engine

### 3.1 Key Name Extraction

Extracts a normalized key name from Ink's raw Key object:

```typescript
// Source: src/keybindings/match.ts (lines 29-47)
export function getKeyName(input: string, key: Key): string | null {
  if (key.escape) return 'escape'
  if (key.return) return 'enter'
  if (key.tab) return 'tab'
  if (key.backspace) return 'backspace'
  if (key.upArrow) return 'up'
  if (key.downArrow) return 'down'
  if (key.wheelUp) return 'wheelup'     // Mouse wheel support
  if (key.wheelDown) return 'wheeldown'
  if (input.length === 1) return input.toLowerCase()
  return null
}
```

Note the inclusion of `wheelup`/`wheeldown` — Claude Code can respond to scroll wheel events in terminals supporting the mouse protocol.

### 3.2 Modifier Matching

Modifier matching includes an important quirk handling:

```typescript
// Source: src/keybindings/match.ts (lines 60-79)
function modifiersMatch(inkMods: InkModifiers, target: ParsedKeystroke): boolean {
  if (inkMods.ctrl !== target.ctrl) return false
  if (inkMods.shift !== target.shift) return false
  // Alt and meta are indistinguishable in the terminal, merged into one logical modifier
  const targetNeedsMeta = target.alt || target.meta
  if (inkMods.meta !== targetNeedsMeta) return false
  // Super (cmd/win) is independent — only kitty keyboard protocol supports it
  if (inkMods.super !== target.super) return false
  return true
}
```

Three key design decisions:
- **Alt/meta merged**: Terminal limitations make them indistinguishable, so `alt` and `meta` in bindings are equivalent
- **Super independent**: The cmd/win key is only available in terminals supporting the kitty keyboard protocol (kitty, WezTerm, ghostty, iTerm2)
- **Escape quirk**: Ink sets `key.meta=true` when Escape is pressed (terminal legacy behavior), so the meta flag must be ignored when matching the Escape key

```typescript
// Source: src/keybindings/match.ts (lines 96-105)
if (key.escape) {
  return modifiersMatch({ ...inkMods, meta: false }, target)  // Escape ignores meta
}
```

---

## 4. Chord Resolver

### 4.1 Chord State Machine

Chord bindings (e.g., `ctrl+k ctrl+s`) require state tracking across keystrokes. `resolveKeyWithChordState` implements a three-stage decision flow:

```typescript
// Source: src/keybindings/resolver.ts (lines 166-244)
export function resolveKeyWithChordState(
  input, key, activeContexts, bindings, pending
): ChordResolveResult {
  // 1. Escape cancels current chord
  if (key.escape && pending !== null) {
    return { type: 'chord_cancelled' }
  }

  // 2. Build current keystroke sequence (existing pending + new keystroke)
  const testChord = pending ? [...pending, currentKeystroke] : [currentKeystroke]

  // 3. Check if it could be a prefix for a longer chord
  // If so, enter chord_started state and wait for subsequent keystrokes
  if (hasLongerChords) {
    return { type: 'chord_started', pending: testChord }
  }

  // 4. Check exact match
  if (exactMatch) {
    return { type: 'match', action: exactMatch.action }
  }

  // 5. No match and not a prefix of any chord
  if (pending !== null) {
    return { type: 'chord_cancelled' }
  }
  return { type: 'none' }
}
```

### 4.2 Chord Priority

When a keystroke is both a single-key binding and a chord prefix, the chord takes priority:

```
ctrl+k → could be a prefix for "ctrl+k ctrl+s"
ctrl+k → could also be a standalone binding

→ Prioritize entering chord_started, waiting for further input
```

But there is a noteworthy exception — the handling of null overrides (unbinds):

```typescript
// Source: src/keybindings/resolver.ts (lines 199-215)
const chordWinners = new Map<string, string | null>()
for (const binding of contextBindings) {
  if (binding.chord.length > testChord.length && chordPrefixMatches(testChord, binding)) {
    chordWinners.set(chordToString(binding.chord), binding.action)
  }
}
let hasLongerChords = false
for (const action of chordWinners.values()) {
  if (action !== null) {
    hasLongerChords = true  // Only non-null bindings count as "longer chords"
    break
  }
}
```

If a user has unbound `ctrl+x ctrl+k` by setting it to `null`, then `ctrl+x` no longer enters chord waiting — the single-key binding on `ctrl+x` can fire normally.

> 📚 **Course Connection**: The chord resolver's state transitions (idle → chord_started → match) look like the Deterministic Finite Automaton (DFA) from *Compiler Principles*, but strictly speaking they are not — a DFA's transition function is `δ(state, input) → next_state`, while `resolveKeyWithChordState`'s transition also depends on the `activeContexts` parameter (the currently active context list), i.e., `δ(state, input, context) → next_state`. This is closer to a **parameterized state machine with guard conditions**: the same keystroke may produce different transition paths under different context sets. The "prefix match vs. exact match" decision for chord bindings is equivalent to a variant of the **maximal munch** rule in lexical analysis — the system prioritizes attempting to match longer chord sequences.

### 4.3 Keystroke Comparison

Keystroke comparison in chord matching also merges alt/meta:

```typescript
// Source: src/keybindings/resolver.ts (lines 107-118)
export function keystrokesEqual(a: ParsedKeystroke, b: ParsedKeystroke): boolean {
  return (
    a.key === b.key &&
    a.ctrl === b.ctrl &&
    a.shift === b.shift &&
    (a.alt || a.meta) === (b.alt || b.meta) &&  // alt+k and meta+k are treated as identical
    a.super === b.super
  )
}
```

---

## 5. Default Bindings

### 5.1 Platform Adaptation

Default bindings are dynamically adjusted based on the platform:

```typescript
// Source: src/keybindings/defaultBindings.ts (lines 15-31)
// Image paste shortcut
const IMAGE_PASTE_KEY = getPlatform() === 'windows' ? 'alt+v' : 'ctrl+v'

// VT mode detection
const SUPPORTS_TERMINAL_VT_MODE =
  getPlatform() !== 'windows' ||
  (isRunningWithBun()
    ? satisfies(process.versions.bun, '>=1.2.23')
    : satisfies(process.versions.node, '>=22.17.0 <23.0.0 || >=24.2.0'))

// Mode cycle key
const MODE_CYCLE_KEY = SUPPORTS_TERMINAL_VT_MODE ? 'shift+tab' : 'meta+m'
```

On Windows, `ctrl+v` is reserved for system paste, so image paste uses `alt+v`. `shift+tab` is unreliable on Windows Terminal without VT mode support, falling back to `meta+m`. VT mode support versions are precise down to Bun 1.2.23 and Node 22.17.0 — this precision comes from tracking upstream PRs.

### 5.2 Global Context

Global bindings are active at all times:

```typescript
// Source: src/keybindings/defaultBindings.ts (lines 33-62)
{
  context: 'Global',
  bindings: {
    'ctrl+c': 'app:interrupt',    // Non-rebindable
    'ctrl+d': 'app:exit',         // Non-rebindable
    'ctrl+l': 'app:redraw',
    'ctrl+t': 'app:toggleTodos',
    'ctrl+o': 'app:toggleTranscript',
    'ctrl+r': 'history:search',
  },
},
```

Source comments explicitly state that `ctrl+c` and `ctrl+d`, though defined here, **cannot be rebound by users** — validation in reservedShortcuts.ts will block attempts.

### 5.3 Chat Context

Chat input has the richest bindings:

```typescript
// Source: src/keybindings/defaultBindings.ts (lines 64-98)
{
  context: 'Chat',
  bindings: {
    escape: 'chat:cancel',
    'ctrl+x ctrl+k': 'chat:killAgents',  // Chord binding — avoids occupying readline editing keys
    [MODE_CYCLE_KEY]: 'chat:cycleMode',
    enter: 'chat:submit',
    'ctrl+_': 'chat:undo',              // Legacy terminals
    'ctrl+shift+-': 'chat:undo',        // Kitty protocol terminals
    'ctrl+x ctrl+e': 'chat:externalEditor',  // Native readline editing binding
    'ctrl+g': 'chat:externalEditor',
    'ctrl+s': 'chat:stash',
  },
},
```

Note that `chat:undo` has two bindings: `ctrl+_` for traditional terminals (sends `\x1f` control character), and `ctrl+shift+-` for kitty keyboard protocol terminals (sends the physical key with modifiers). Two bindings for the same action ensure cross-terminal compatibility.

`ctrl+x ctrl+k` uses a chord rather than a single key because — `ctrl+a`, `ctrl+b`, `ctrl+e`, `ctrl+f`, etc. are readline editing keys (start of line, previous character, end of line, next character) and cannot be occupied. Using `ctrl+x` as a chord prefix avoids conflicts with readline.

### 5.4 Feature Gates

Some bindings are controlled by feature flags:

```typescript
// Source: src/keybindings/defaultBindings.ts (lines 45-59)
...(feature('KAIROS') || feature('KAIROS_BRIEF')
  ? { 'ctrl+shift+b': 'app:toggleBrief' as const }
  : {}),
...(feature('VOICE_MODE') ? { space: 'voice:pushToTalk' } : {}),
...(feature('TERMINAL_PANEL') ? { 'meta+j': 'app:toggleTerminal' } : {}),
```

The spread + conditional object pattern enables runtime conditional binding injection — `feature('KAIROS')` and similar calls query the GrowthBook feature gate at runtime, rather than being compile-time constants. The binding set is dynamically constructed on each call to `getDefaultParsedBindings()` based on the current feature flag state.

---

## 6. User Customization and Loading

### 6.1 Loading Flow

User bindings are appended after default bindings, with "last wins" semantics:

```typescript
// Source: src/keybindings/loadUserBindings.ts (lines 133-216)
export async function loadKeybindings(): Promise<KeybindingsLoadResult> {
  const defaultBindings = getDefaultParsedBindings()

  // Non-Anthropic employees skip user config
  if (!isKeybindingCustomizationEnabled()) {
    return { bindings: defaultBindings, warnings: [] }
  }

  const content = await readFile(userPath, 'utf-8')
  const parsed = jsonParse(content)

  // Extract from { "bindings": [...] } format
  let userBlocks = (parsed as { bindings: unknown }).bindings

  const userParsed = parseBindings(userBlocks)
  // User bindings come after, overriding defaults
  const mergedBindings = [...defaultBindings, ...userParsed]

  // Validation + duplicate key detection
  const duplicateKeyWarnings = checkDuplicateKeysInJson(content)
  const warnings = [...duplicateKeyWarnings, ...validateBindings(userBlocks, mergedBindings)]

  return { bindings: mergedBindings, warnings }
}
```

The "last wins" implementation is remarkably simple — array concatenation, with the last match taken during parsing. This is much cleaner than maintaining an override map, and is conceptually identical to CSS cascading's "later declaration wins" mechanism.

### 6.2 Hot Reloading

The configuration file is watched with chokidar, supporting real-time updates without restart:

```typescript
// Source: src/keybindings/loadUserBindings.ts (lines 353-404)
export async function initializeKeybindingWatcher(): Promise<void> {
  watcher = chokidar.watch(userPath, {
    persistent: true,
    ignoreInitial: true,
    awaitWriteFinish: {
      stabilityThreshold: FILE_STABILITY_THRESHOLD_MS,  // 500ms
      pollInterval: FILE_STABILITY_POLL_INTERVAL_MS,     // 200ms
    },
    ignorePermissionErrors: true,
    usePolling: false,
    atomic: true,
  })

  watcher.on('add', handleChange)
  watcher.on('change', handleChange)
  watcher.on('unlink', handleDelete)  // File deleted → reset to defaults
}
```

The `awaitWriteFinish` configuration ensures the file is fully written before triggering a reload — editors may perform multiple fsync writes, and the 500ms stability threshold prevents reading a half-written file.

> 📚 **Course Connection**: The `awaitWriteFinish` stability threshold mechanism is a classic application of **debouncing** from *Operating Systems* — isomorphic to button debouncing in hardware interrupt handling. When an editor saves a file, it may trigger multiple `write` system calls (truncate → write → fsync), and the file watcher needs to wait for the write sequence to stabilize before responding, otherwise it would read incomplete file contents. This also touches on the **atomic write** problem from OS courses — `rename` is atomic, but `write` is not.

### 6.3 Unbinding Mechanism

Users can unbind default shortcuts by setting them to `null`:

```typescript
// Definition in schema
z.union([
  z.enum(KEYBINDING_ACTIONS),
  z.string().regex(/^command:[a-zA-Z0-9:\-_]+$/),
  z.null().describe('Set to null to unbind a default shortcut'),
])
```

During parsing, bindings with a `null` action also participate in matching — if matched to `null`, `{ type: 'unbound' }` is returned instead of `{ type: 'none' }, letting the upper layer know this key was explicitly unbound (rather than having no binding).

---

## 7. Validation System

### 7.1 Reserved Shortcuts

Certain shortcuts are marked as non-rebindable:

```typescript
// Source: src/keybindings/reservedShortcuts.ts (lines 16-33)
export const NON_REBINDABLE: ReservedShortcut[] = [
  { key: 'ctrl+c', reason: 'Used for interrupt/exit (hardcoded)', severity: 'error' },
  { key: 'ctrl+d', reason: 'Used for exit (hardcoded)', severity: 'error' },
  { key: 'ctrl+m', reason: 'Same as Enter in terminal (both send CR)', severity: 'error' },
]

export const TERMINAL_RESERVED: ReservedShortcut[] = [
  { key: 'ctrl+z', reason: 'Unix process suspend (SIGTSTP)', severity: 'warning' },
  { key: 'ctrl+\\', reason: 'Terminal quit signal (SIGQUIT)', severity: 'error' },
]

export const MACOS_RESERVED: ReservedShortcut[] = [
  { key: 'cmd+c', reason: 'macOS system copy', severity: 'error' },
  { key: 'cmd+v', reason: 'macOS system paste', severity: 'error' },
  // ...
]
```

Note the reason for `ctrl+m` being reserved: in the terminal, both `Ctrl+M` and `Enter` send CR (carriage return); they are indistinguishable at the terminal level.

### 7.2 JSON Duplicate Key Detection

JSON technically allows duplicate keys (later overrides earlier), but this is usually a user error. The validator detects them by regex-matching the raw JSON string:

```typescript
// Source: src/keybindings/validate.ts (lines 258-307)
export function checkDuplicateKeysInJson(jsonString: string): KeybindingWarning[] {
  const bindingsBlockPattern = /"bindings"\s*:\s*\{([^{}]*(?:\{[^{}]*\}[^{}]*)*)\}/g
  // ... Search for duplicate keys within each bindings block
  const keyPattern = /"([^"]+)"\s*:/g
  const keysByName = new Map<string, number>()
  // ...
}
```

This detection must happen before `JSON.parse`, because duplicate keys are silently discarded after parsing.

### 7.3 Keystroke String Normalization

Keystroke bindings need to be normalized when compared, so that `ctrl+shift+k` and `shift+ctrl+k` are not treated as different:

```typescript
// Source: src/keybindings/reservedShortcuts.ts (lines 91-127)
export function normalizeKeyForComparison(key: string): string {
  return key.trim().split(/\s+/).map(normalizeStep).join(' ')
}

function normalizeStep(step: string): string {
  const parts = step.split('+')
  const modifiers: string[] = []
  let mainKey = ''
  for (const part of parts) {
    // Normalize modifier names: control→ctrl, option/opt→alt, command/cmd→cmd
    // ...
  }
  modifiers.sort()  // Sort modifiers
  return [...modifiers, mainKey].join('+')
}
```

Modifier name normalization + sorting ensures that `ctrl+shift+k` and `shift+control+k` are treated as equivalent.

### 7.4 Voice Binding Warning

There is special validation for `voice:pushToTalk` — bare letter key bindings can cause problems:

```typescript
// Source: src/keybindings/validate.ts (lines 220-243)
if (action === 'voice:pushToTalk') {
  const ks = parseChord(key)[0]
  if (ks && !ks.ctrl && !ks.alt && !ks.shift && !ks.meta && !ks.super
      && /^[a-z]$/.test(ks.key)) {
    warnings.push({
      severity: 'warning',
      message: `Binding "${key}" to voice:pushToTalk will print to the input box during warm-up`,
      suggestion: 'Use space or a modifier combination (e.g., meta+k)',
    })
  }
}
```

Push-to-talk requires detecting a key being held down. If bound to a bare letter key, key repeats will print characters into the input box before activation.

---

## 8. Telemetry

A telemetry event is recorded when user custom bindings are loaded, but limited to once per day:

```typescript
// Source: src/keybindings/loadUserBindings.ts (lines 83-90)
function logCustomBindingsLoadedOncePerDay(userBindingCount: number): void {
  const today = new Date().toISOString().slice(0, 10)
  if (lastCustomBindingsLogDate === today) return
  lastCustomBindingsLogDate = today
  logEvent('tengu_custom_keybindings_loaded', {
    user_binding_count: userBindingCount,
  })
}
```

This lets the team estimate the proportion of users with custom keybindings, without triggering telemetry excessively due to hot reloading.

---

## 9. Context Resolution Flow Summary

The complete path from a key press to an action:

```
User presses ctrl+k
     │
     ▼
Ink framework produces { input: 'k', key: { ctrl: true, ... } }
     │
     ▼
getKeyName() → 'k'
matchesKeystroke() → checks modifier match
     │
     ▼
resolveKeyWithChordState(input, key, ['Chat', 'Global'], bindings, null)
     │
     ├─ Found longer chord prefix (ctrl+k ctrl+s exists)
     │  → return { type: 'chord_started', pending: [ctrl+k] }
     │
     ▼ (User continues pressing ctrl+s)
     │
resolveKeyWithChordState(input, key, ['Chat', 'Global'], bindings, [ctrl+k])
     │
     ├─ Exact match ctrl+k ctrl+s → 'chat:killAgents'
     │  → return { type: 'match', action: 'chat:killAgents' }
     │
     ▼
UI layer executes chat:killAgents action
```

---

## Critical Analysis

### Limitations

1. **Feature gate restriction**: Keybinding customization is currently only available to Anthropic employees (`isKeybindingCustomizationEnabled()` checks a GrowthBook feature gate). External users always use the default bindings and cannot customize them.

2. **No conditional bindings**: There is no support for VS Code-style `when` conditional expressions (e.g., `"when": "editorTextFocus && !editorReadonly"`). Contexts are a predefined fixed set with no composition capability.

3. **Chord depth limitation**: While theoretically supporting chords of arbitrary depth, in practice only 2 levels are used (e.g., `ctrl+x ctrl+k`). Deeper chords provide poor UX in a terminal environment.

4. **Terminal capability limitations**: The `super` (cmd/win) key is only available in terminals supporting the kitty keyboard protocol, so a large number of terminals cannot use `cmd+` bindings. Alt and meta are indistinguishable at the terminal level, limiting the number of available modifier combinations.

### Design Trade-offs

1. **"Last wins" vs. explicit override**: The array concatenation priority mechanism is simple but implicit — users must understand that their bindings are appended after defaults in order to override them. There is no "explicit override declaration" mechanism, nor a fallback semantics of "only take effect when no default binding exists."

2. **Zod validation vs. loose parsing**: Schema validation is strict enough to reject unknown fields. This may cause forward compatibility problems when the configuration format evolves — fields added in newer versions will be rejected by older versions.

3. **Synchronous loading + asynchronous hot reloading**: Initial loading is synchronous (`readFileSync`) because it is called inside React's `useState` initializer. Subsequent hot reloading is asynchronous. The code for the two paths is almost completely duplicated (`loadKeybindings` and `loadKeybindingsSyncWithWarnings`), creating maintenance overhead.

4. **Escape's meta quirk**: Ink setting `meta=true` on Escape is a terminal legacy issue, and the system needs special handling in both the matching layer (match.ts) and the resolution layer (resolver.ts). This is an external constraint that must be worked around.

### Overall Assessment

The keybinding system demonstrates a thorough understanding of terminal environment complexities — from Windows VT mode version tracking to `ctrl+m`/Enter equivalence warnings, these details reflect solid terminal compatibility engineering experience (similar handling can be found in terminal tools like tmux and neovim). The chord resolver's null-unbind priority handling is well designed. The system's main weaknesses are the feature gate preventing most users from accessing customization, and the maintenance burden of the synchronous/asynchronous dual-path code.
