Analyzed: 14 files, ~3,159 lines of code Date: 2026-04-02 Scope: Complete keybinding architecture, bindings inventory, security, platform differences
The keybindings system follows a modular, configuration-driven architecture:
-
Default Bindings Layer (
defaultBindings.ts)- Hard-coded platform-specific defaults
- Feature-gated bindings (enabled via feature flags)
- Acts as base layer before user overrides
-
User Config Layer (
loadUserBindings.ts)- Loads from
~/.claude/keybindings.json(Anthropic employees only) - Watched for hot-reload with chokidar
- Merged on top of defaults (last wins)
- Gated by
tengu_keybinding_customization_releasefeature flag
- Loads from
-
Resolution Pipeline (
resolver.ts,match.ts)- Single-key matching (Phase 1)
- Multi-key chord sequences (Phase 2) with timeout
- Context-based priority (specific contexts > Global)
- Returns actions for dispatch or null for unbound keys
-
React Integration (
KeybindingContext.tsx,KeybindingProviderSetup.tsx)- Context-based handler registration
- Dual mode: ref-based (sync) + state-based (UI updates)
- ChordInterceptor pre-processes chord keystrokes
- useKeybinding hooks for components
-
Validation & Safety (
validate.ts,reservedShortcuts.ts)- Pre-binding validation (syntax, context, action)
- Reserved shortcut detection (OS-level, terminal)
- Non-rebindable keys (ctrl+c, ctrl+d, ctrl+m)
ctrl+c→app:interrupt⚠️ HARDCODED, non-rebindable- Uses double-press time-based detection (see comments)
- Cannot be overridden by user config
ctrl+d→app:exit⚠️ HARDCODED, non-rebindable- Cannot be overridden by user config
ctrl+l→app:redraw- Full terminal redraw
ctrl+t→app:toggleTodos- Show/hide todo sidebarctrl+o→app:toggleTranscript- View message transcriptctrl+shift+o→app:toggleTeammatePreview- Team collaboration viewctrl+shift+b→app:toggleBrief- FEATURE-GATED (KAIROS | KAIROS_BRIEF)meta+j→app:toggleTerminal- FEATURE-GATED (TERMINAL_PANEL)
ctrl+shift+f→app:globalSearch- Search entire codebasecmd+shift+f→app:globalSearch- (kitty protocol terminals only)ctrl+shift+p→app:quickOpen- Quick file/command openercmd+shift+p→app:quickOpen- (kitty protocol)
ctrl+r→history:search- Open command history search
enter→chat:submit- Send messageescape→chat:cancel- Clear/cancel input
up→history:previous- Previous command in historydown→history:next- Next command in history
shift+tab→chat:cycleMode(most platforms) - Cycle input modesmeta+m→chat:cycleMode- Windows fallback (no VT mode support)meta+p→chat:modelPicker- Open model selection menumeta+o→chat:fastMode- Toggle fast/full reasoning modemeta+t→chat:thinkingToggle- Toggle extended thinking
ctrl+_→chat:undo- Undo last input (legacy terminals)ctrl+shift+-→chat:undo- Undo (Kitty protocol)ctrl+x ctrl+e→chat:externalEditor- Open external editorctrl+g→chat:externalEditor- Shorter binding for same actionctrl+s→chat:stash- Save input draft
alt+v→chat:imagePaste(Windows) - Paste image from clipboardctrl+v→chat:imagePaste(Mac/Linux) - Paste imagespace→voice:pushToTalk- FEATURE-GATED (VOICE_MODE)- Hold-to-talk voice activation
ctrl+x ctrl+k→chat:killAgents- Stop running agents
shift+up→chat:messageActions- FEATURE-GATED (MESSAGE_ACTIONS)- Access message action menu
tab→autocomplete:accept- Accept suggestionescape→autocomplete:dismiss- Close autocomplete menuup→autocomplete:previous- Previous suggestiondown→autocomplete:next- Next suggestion
y→confirm:yes- Confirm yesn→confirm:no- Confirm noenter→confirm:yes- Alternative confirmationescape→confirm:no- Cancel dialogup→confirm:previous- Navigate dialog options updown→confirm:next- Navigate dialog options downtab→confirm:nextField- Move to next form fieldspace→confirm:toggle- Toggle option/checkboxshift+tab→confirm:cycleMode- Cycle mode in dialogsctrl+e→confirm:toggleExplanation- Show/hide permission explanationctrl+d→permission:toggleDebug- DEBUG - Toggle debug info in permission dialogs
escape→confirm:no- Close settingsup→select:previous- Navigate updown→select:next- Navigate downk→select:previous- vim-style upj→select:next- vim-style downctrl+p→select:previous- Emacs-style upctrl+n→select:next- Emacs-style downspace→select:accept- Toggle settingenter→settings:close- Save and close/→settings:search- Enter search moder→settings:retry- Retry loading (on error only)
tab→tabs:next- Next tabshift+tab→tabs:previous- Previous tabright→tabs:next- Arrow key navigationleft→tabs:previous- Arrow key navigation
escape→transcript:exit- Exit transcript viewctrl+c→transcript:exit- Alternative exit (interrupt-style)q→transcript:exit- Standard pager convention (less, tmux)ctrl+e→transcript:toggleShowAll- Show/hide all messages
ctrl+r→historySearch:next- Find next matchescape→historySearch:accept- Accept selectedtab→historySearch:accept- Alternative acceptctrl+c→historySearch:cancel- Cancel searchenter→historySearch:execute- Execute selected command
ctrl+b→task:background- Send running task to background- Note: In tmux, users must press ctrl+b twice (tmux prefix escapes)
ctrl+t→theme:toggleSyntaxHighlighting- Toggle syntax highlighting- Conflicts with Global
ctrl+t→app:toggleTodos(Context Priority!)
- Conflicts with Global
pageup→scroll:pageUp- Scroll up one pagepagedown→scroll:pageDown- Scroll down one pagewheelup→scroll:lineUp- Scroll up (mouse wheel)wheeldown→scroll:lineDown- Scroll down (mouse wheel)ctrl+home→scroll:top- Jump to topctrl+end→scroll:bottom- Jump to bottomctrl+shift+c→selection:copy- Copy selected textcmd+c→selection:copy- (kitty protocol terminals)
escape→help:dismiss- Close help overlay
right→attachments:next- Next attachmentleft→attachments:previous- Previous attachmentbackspace→attachments:remove- Remove selecteddelete→attachments:remove- Alternative removedown→attachments:exit- Exit attachment viewescape→attachments:exit- Alternative exit
up→footer:up- Navigate footer items upctrl+p→footer:up- Emacs-style updown→footer:down- Navigate footer items downctrl+n→footer:down- Emacs-style downright→footer:next- Next indicatorleft→footer:previous- Previous indicatorenter→footer:openSelected- Open/expand selectedescape→footer:clearSelection- Clear selection
up→messageSelector:up- Previous messagedown→messageSelector:down- Next messagek→messageSelector:up- vim-style upj→messageSelector:down- vim-style downctrl+p→messageSelector:up- Emacs-style upctrl+n→messageSelector:down- Emacs-style downctrl+up→messageSelector:top- Jump to firstshift+up→messageSelector:top- Alternative firstmeta+up→messageSelector:top- Alternative first (cmd on macOS)shift+k→messageSelector:top- vim-style firstctrl+down→messageSelector:bottom- Jump to lastshift+down→messageSelector:bottom- Alternative lastmeta+down→messageSelector:bottom- Alternative lastshift+j→messageSelector:bottom- vim-style lastenter→messageSelector:select- Select and apply
up→messageActions:prev- Previous messagedown→messageActions:next- Next messagek→messageActions:prev- vim-style upj→messageActions:next- vim-style downmeta+up→messageActions:top- Jump to first (cmd/super)meta+down→messageActions:bottom- Jump to lastsuper+up→messageActions:top- (kitty protocol)super+down→messageActions:bottom- (kitty protocol)shift+up→messageActions:prevUser- Previous user messageshift+down→messageActions:nextUser- Next user messageescape→messageActions:escape- Cancel actionctrl+c→messageActions:ctrlc- Interrupt (mirrors ctrl+c behavior)enter→messageActions:enter- Confirm actionc→messageActions:c- Shortcut keyp→messageActions:p- Shortcut key
escape→diff:dismiss- Close diff viewerleft→diff:previousSource- Previous diff sourceright→diff:nextSource- Next diff sourceup→diff:previousFile- Previous file in diffdown→diff:nextFile- Next file in diffenter→diff:viewDetails- View details/expand
left→modelPicker:decreaseEffort- Reduce model effort (ant-only)right→modelPicker:increaseEffort- Increase model effort
up→select:previous- Navigate updown→select:next- Navigate downj→select:next- vim-style downk→select:previous- vim-style upctrl+n→select:next- Emacs-style nextctrl+p→select:previous- Emacs-style previousenter→select:accept- Select/confirmescape→select:cancel- Cancel
space→plugin:toggle- Enable/disable plugini→plugin:install- Install plugin
HIDDEN & DEBUG KEYBINDINGS
Explicitly Hidden (Not in Help)
permission:toggleDebug(ctrl+din Confirmation context)- Purpose: Debug permission dialog information
- Exposure: Only available in permission dialogs
- Security: Non-sensitive (shows internal permission state)
- System has fallback display text for actions that don't resolve from config
- Logs
tengu_keybinding_fallback_usedevent (for migration monitoring) - Only happens if bindings fail to load or action not found
- Logs
- Space key handling:
- Default:
voice:pushToTalkwhen VOICE_MODE enabled - Validation catches bare letter bindings (like 'a') for voice (prints during warmup)
- System warns if voice:pushToTalk bound to bare letter key
- Default:
- Windows:
alt+v(ctrl+v is system paste on Windows terminal) - Mac/Linux:
ctrl+v(standard in *nix terminals)
- Windows (VT mode enabled):
shift+tab(Node ≥24.2.0 / 22.17.0 or Bun ≥1.2.23) - Windows (no VT mode):
meta+m(Fallback - Modifier-only chords fail without VT) - Mac/Linux:
shift+tab(Always supported)
- Alt vs Opt: opt on macOS, alt elsewhere
- cmd vs super: cmd on macOS, super elsewhere
- Only arrives via kitty keyboard protocol (kitty, WezTerm, ghostty, iTerm2)
- Bindings with cmd/super simply never fire on non-kitty terminals
- Not a silently-ignored modifier—the keystroke literally won't match
The system includes extensive vim keybindings across multiple contexts:
- j → Next/down
- k → Previous/up
- ctrl+n → Next
- ctrl+p → Previous
- shift+k → Jump to top
- shift+j → Jump to bottom
- ctrl+up / shift+up → Jump to top
- ctrl+down / shift+down → Jump to bottom
Multiple bindings for same action reduce muscle memory switching:
- Arrow keys for unknown users
- vim (j/k) for vim users
- Emacs (ctrl+n/p) for Emacs users
Input (string, Key) → Match ParsedKeystroke against active contexts → Last match wins → Return result
- Check if chord could START (is prefix of longer chords)
- Check for EXACT MATCH of full chord
- If pending chord: cancel on escape or invalid key
- Default: {type: 'none'} (let other handlers process)
Timeout: 1000ms - if user doesn't complete chord in time, cancel
Contexts: [RegisteredContexts, ComponentContext, Global] Deduplicated, preserving order (first occurrence wins) Later contexts override earlier
- Get all handlers registered for action
- Find first handler whose context is ACTIVE
- Invoke handler synchronously
- Multi-keystroke bindings shadow single-key bindings
- Define
ctrl+x ctrl+k→chat:killAgents ctrl+xbecomes "wait for next key" (chord pending)- User can null-unbind
ctrl+x ctrl+kto makectrl+xsingle-key
When context "ThemePicker" is active:
- ThemePicker: { ctrl+t → toggle:syntax }
- Global: { ctrl+t → app:toggleTodos }
- ThemePicker wins (more specific)
User bindings always come AFTER defaults in merged list
Finding: NO BYPASS RISK
- Keybindings only trigger CONFIGURED actions
- Actions do NOT include: file access, API calls, data exfiltration
Threat Model 2: Hidden Commands via Keybinding
Finding: LIMITED RISK (mitigated)
- Command bindings are Chat-context only
- Restricted by application logic
- All command bindings go through normal validation
Finding: CRITICAL KEYS PROTECTED
- ctrl+c → app:interrupt (Cannot override)
- ctrl+d → app:exit (Cannot override)
- ctrl+m → Cannot override (terminal limitation)
Finding: CONFIGURABLE WITH WARNINGS
- voice:pushToTalk on bare letters warns
- System validates bare letter keys (a-z)
- Recommendation: use space or modifier combos
Finding: INJECTION IMPOSSIBLE
- Keybindings loaded from ~/.claude/keybindings.json only
- File requires valid JSON
- Validation runs before resolution
Finding: HANDLED GRACEFULLY
- Parse errors fall back to defaults
- Hot-reload catches errors and notifies user
- KAIROS | KAIROS_BRIEF: ctrl+shift+b → app:toggleBrief
- TERMINAL_PANEL: meta+j → app:toggleTerminal
- QUICK_SEARCH: ctrl+shift+f/p, cmd+shift+f/p
- VOICE_MODE: space → voice:pushToTalk
- MESSAGE_ACTIONS: Entire MessageActions context (13 bindings)
- tengu_keybinding_customization_release (loadUserBindings.ts)
- Currently: Anthropic employees only (USER_TYPE === 'ant')
- External users: Always use defaults (no customization)
- Syntax Validation - Check for empty parts, ensure parsing
- Context Validation - Must be one of 18 defined contexts
- Action Validation - Must be known action OR command:* OR null
- Voice Mode Validation - Warns for bare letter keys
- Duplicate Detection - Checks raw JSON for duplicates
- Reserved Shortcut Detection - Non-rebindable, terminal control, macOS system keys
- parse_error - Syntax error
- duplicate - Key appears multiple times
- reserved - Key unlikely to work (OS/terminal intercepts)
- invalid_context - Unknown context name
- invalid_action - Unknown action or wrong context
-
Remove fallback parameter after migration (keybindings-migration)
- Fallback display text exists as safety net
- Plan: Once stable, remove defensive pattern
- Telemetry: 'tengu_keybinding_fallback_used' events tracking
-
Chord Timeout Hardcoded (1000ms)
- No user configuration for timeout
- Potential TODO: Make configurable in keybindings.json
- Escape Key: Ink sets key.meta=true when escape pressed (legacy behavior)
- Alt/Meta Collapse: Can't distinguish at TTY level
- Super/Cmd Limitation: Only arrives via kitty keyboard protocol
- VT Mode Windows Terminal: Modifier-only chords fail without VT mode
- readline Editing Keys: ctrl+x prefix used to avoid shadowing
- Chord Prefix Masking: If "ctrl+x ctrl+k" is bound, "ctrl+x" enters chord-wait
- Fallback to Defaults: Invalid keybindings.json falls back to defaults
- App-level: app:* (10 actions)
- Chat input: chat:* (13 actions)
- History: history:* (2 actions)
- Autocomplete: autocomplete:* (4 actions)
- Confirmation: confirm:* (8 actions)
- And 20+ more categories (tabs, transcript, search, task, theme, help, etc.)
Location: ~/.claude/keybindings.json
{
"$schema": "https://www.schemastore.org/claude-code-keybindings.json",
"$docs": "https://code.claude.com/docs/en/keybindings",
"bindings": [
{
"context": "Chat",
"bindings": {
"ctrl+alt+a": "chat:submit",
"ctrl+k": null
}
}
]
}- Bindings override defaults (last wins)
- Null value unbinds a default binding
- Command bindings format: "command:help", "command:compact"
- Only available if tengu_keybinding_customization_release feature enabled
Single action binding with context and active state options
Multiple actions in one hook call
Display configured shortcut or fallback text
Register context priority while component mounted
-
tengu_custom_keybindings_loaded
- Fired once per day when user custom bindings loaded
- Metric: user_binding_count
-
tengu_keybinding_fallback_used
- Fired when display text lookup fails
- Metrics: action, context, fallback, reason
- Total Contexts: 18
- Total Actions: 97+
- Total Default Bindings: ~220 (varies by features enabled)
- Platform-specific Paths: 2 (Windows vs Mac/Linux)
- Feature-gated Bindings: ~30
- Non-rebindable Keys: 3 (ctrl+c, ctrl+d, ctrl+m)
- Reserved (Warning) Keys: 7 additional OS/terminal keys
- vim Keybindings: 12+ across multiple contexts
- Files Analyzed: 14 (3,159 lines)