claude-code/spec/11_special_systems.md
2026-04-01 01:20:27 +05:30

63 KiB
Raw Permalink Blame History

Claude Code — Special Systems: Buddy, Memory, Keybindings, Skills, Voice, Plugins & More


Table of Contents

  1. Buddy (Companion/Tamagotchi) System
  2. Memory Directory (memdir) System
  3. Keybindings System
  4. Skills System
  5. Voice System
  6. Plugins System
  7. Output Styles
  8. Hooks Schema
  9. Native-TypeScript Ports (native-ts/)
  10. MoreRight Hook (moreright/)
  11. Migrations
  12. Core Type Definitions (types/)
  13. Remote Session System (remote/)

1. Buddy (Companion/Tamagotchi) System

1.1 System Overview

The Buddy system is a virtual companion ("tamagotchi") that appears as an ASCII art sprite beside the user's prompt input. Each user receives a deterministic companion whose appearance (species, eyes, hat, rarity, shiny status, stats) is derived from their user ID via a seeded PRNG — not stored. The companion's "soul" (name and personality) is generated by the AI and persisted in config. The feature is gated behind a BUDDY feature flag and was planned to launch April 17, 2026 as a "teaser window."

Source directory: src/buddy/

Files:

  • buddy/types.ts — Type definitions, species/eyes/hats/stats constants, rarity weights
  • buddy/companion.ts — PRNG, rolling, and companion reconstruction logic
  • buddy/sprites.ts — ASCII art sprite frames for all 18 species
  • buddy/prompt.ts — Companion introduction text injection into system prompt
  • buddy/useBuddyNotification.tsx — React hook for startup teaser notification
  • buddy/CompanionSprite.tsx — React component rendering the animated sprite + speech bubble

1.2 Architecture

The system splits companion data into two parts:

  • Bones (CompanionBones): Deterministic visual/stat data derived from hash(userId + SALT). Never stored; always recomputed on read. This prevents users from editing their config to claim a legendary rarity.
  • Soul (CompanionSoul): AI-generated name and personality, persisted to config.companion as StoredCompanion.
  • Full Companion (Companion = CompanionBones & CompanionSoul & { hatchedAt: number }): assembled at read time by getCompanion().

The rolling process uses a single pass through a seeded PRNG (mulberry32) to deterministically select:

  1. Rarity tier (weighted random)
  2. Species
  3. Eye style
  4. Hat (none if common)
  5. Shiny flag (1% probability)
  6. Stats (one peak stat, one dump stat, rest scattered; floor scales with rarity)
  7. inspirationSeed (passed to AI for soul generation)

1.3 Species Encoding (Canary Bypass)

Critical detail: Species name strings are encoded as String.fromCharCode(...) literals to avoid triggering a build-time string scan that checks for model codenames (excluded-strings.txt). One species name collides with an internal model codename canary. All 18 species use this encoding uniformly.

// In buddy/types.ts:
const c = String.fromCharCode
export const duck = c(0x64,0x75,0x63,0x6b) as 'duck'
// ... all 18 species encoded this way

1.4 PRNG: Mulberry32

Algorithm:

function mulberry32(seed: number): () => number {
  let a = seed >>> 0
  return function () {
    a |= 0
    a = (a + 0x6d2b79f5) | 0
    let t = Math.imul(a ^ (a >>> 15), 1 | a)
    t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t
    return ((t ^ (t >>> 14)) >>> 0) / 4294967296
  }
}
  • Tiny 32-bit PRNG with good statistical distribution
  • Seeded from hashString(userId + SALT) where SALT = 'friend-2026-401'
  • hashString uses Bun's native hash when available, falls back to FNV-1a (32-bit)

Caching: The roll() function memoizes the last result keyed on userId + SALT, since it is called from three hot paths: the 500ms sprite tick, per-keystroke PromptInput rendering, and the per-turn observer.


1.5 Data Structures

Rarity

export const RARITIES = ['common', 'uncommon', 'rare', 'epic', 'legendary'] as const
export type Rarity = (typeof RARITIES)[number]

RARITY_WEIGHTS

export const RARITY_WEIGHTS = {
  common: 60,    // 60%
  uncommon: 25,  // 25%
  rare: 10,      // 10%
  epic: 4,       // 4%
  legendary: 1,  // 1%
} as const

RARITY_FLOOR (stat minimums per rarity)

const RARITY_FLOOR: Record<Rarity, number> = {
  common: 5,
  uncommon: 15,
  rare: 25,
  epic: 35,
  legendary: 50,
}

RARITY_STARS (UI display)

export const RARITY_STARS = {
  common: '★',
  uncommon: '★★',
  rare: '★★★',
  epic: '★★★★',
  legendary: '★★★★★',
}

RARITY_COLORS (maps to Theme keys)

export const RARITY_COLORS = {
  common: 'inactive',
  uncommon: 'success',
  rare: 'permission',
  epic: 'autoAccept',
  legendary: 'warning',
}

Species (18 total)

export const SPECIES = [
  duck, goose, blob, cat, dragon, octopus, owl, penguin,
  turtle, snail, ghost, axolotl, capybara, cactus, robot,
  rabbit, mushroom, chonk,
] as const
export type Species = (typeof SPECIES)[number]

Eye (6 styles)

export const EYES = ['·', '✦', '×', '◉', '@', '°'] as const

Hat (8 types)

export const HATS = [
  'none', 'crown', 'tophat', 'propeller',
  'halo', 'wizard', 'beanie', 'tinyduck',
] as const

StatName (5 stats)

export const STAT_NAMES = ['DEBUGGING', 'PATIENCE', 'CHAOS', 'WISDOM', 'SNARK'] as const

CompanionBones (deterministic, never stored)

export type CompanionBones = {
  rarity: Rarity
  species: Species
  eye: Eye
  hat: Hat
  shiny: boolean
  stats: Record<StatName, number>  // 1100 per stat
}

CompanionSoul (AI-generated, stored)

export type CompanionSoul = {
  name: string
  personality: string
}

StoredCompanion (what persists in config)

export type StoredCompanion = CompanionSoul & { hatchedAt: number }

Companion (runtime assembled)

export type Companion = CompanionBones & CompanionSoul & { hatchedAt: number }

Roll (output of rollFrom)

export type Roll = {
  bones: CompanionBones
  inspirationSeed: number  // Passed to AI for deterministic soul generation
}

1.6 Stat Rolling Algorithm

function rollStats(rng, rarity): Record<StatName, number> {
  const floor = RARITY_FLOOR[rarity]  // 550 depending on rarity
  const peak = pick(rng, STAT_NAMES)  // one stat gets +50 bonus
  let dump = pick(rng, STAT_NAMES)    // different stat, penalized
  while (dump === peak) dump = pick(rng, STAT_NAMES)

  for (name of STAT_NAMES) {
    if (name === peak)   stats[name] = min(100, floor + 50 + rng()*30)    // 55130, capped 100
    else if (name === dump) stats[name] = max(1, floor - 10 + rng()*15)   // 120 (at common)
    else                 stats[name] = floor + rng()*40                    // spread
  }
}

1.7 Sprite System

Each species has 3 animation frames of ASCII art, each 5 lines tall, 12 chars wide. The {E} placeholder in frame templates is substituted with the companion's eye character.

Frame structure:

  • Line 0: Hat slot (blank if no hat, or ambient detail like ~ for smoke, * for sparks)
  • Lines 14: Body art

Hat rendering: Hats are placed on line 0 only if it is blank. If all frames have a blank line 0, the blank line is stripped (saves terminal row when no hat).

Hat ASCII art (in HAT_LINES):

Hat ASCII
none (empty)
crown \^^^/
tophat [___]
propeller -+-
halo ( )
wizard /^\
beanie (___)
tinyduck ,>

Idle animation sequence:

const IDLE_SEQUENCE = [0, 0, 0, 0, 1, 0, 0, 0, -1, 0, 0, 2, 0, 0, 0]
// -1 = blink on frame 0 (special)
// TICK_MS = 500ms per step

Pet animation: After /buddy pet, hearts float upward for 5 ticks (~2.5 seconds):

const PET_HEARTS = [
  `   ${H}    ${H}   `,
  `  ${H}  ${H}   ${H}  `,
  ` ${H}   ${H}  ${H}   `,
  `${H}  ${H}      ${H} `,
  '·    ·   ·  '
]

1.8 All Exports

buddy/companion.ts

Export Type Description
Roll type { bones: CompanionBones; inspirationSeed: number }
roll(userId: string) () => Roll Deterministic roll (memoized per userId+SALT)
rollWithSeed(seed: string) () => Roll Roll from arbitrary seed string
companionUserId() () => string Returns oauthAccount UUID or userID or 'anon'
getCompanion() () => Companion | undefined Assembles companion from config + roll

buddy/sprites.ts

Export Type Description
renderSprite(bones, frame?) (CompanionBones, number?) => string[] Returns array of lines for the sprite
spriteFrameCount(species) (Species) => number Number of frames for a species (all are 3)
renderFace(bones) (CompanionBones) => string Short face string for inline display

buddy/prompt.ts

Export Type Description
companionIntroText(name, species) (string, string) => string System prompt text for companion introduction
getCompanionIntroAttachment(messages?) (Message[]?) => Attachment[] Returns intro attachment if not yet injected

buddy/useBuddyNotification.tsx

Export Type Description
isBuddyTeaserWindow() () => boolean True if local date is April 17, 2026
isBuddyLive() () => boolean True if local date is April 2026 or later
useBuddyNotification() () => void React hook: shows rainbow /buddy startup hint
findBuddyTriggerPositions(text) (string) => Array<{start,end}> Find /buddy occurrences in text

1.9 Configuration

  • config.companion — Stores StoredCompanion (name, personality, hatchedAt)
  • config.companionMuted — When true, suppresses companion intro injection
  • Feature flag: feature('BUDDY') — gates all buddy functionality
  • SALT constant: 'friend-2026-401' — mixed into hash to prevent replay attacks

2. Memory Directory (memdir) System

2.1 System Overview

The memory system provides Claude with persistent, file-based memory across sessions. Memory is stored as markdown files with YAML frontmatter in a per-project directory. It encompasses:

  1. Auto memory (~/.claude/projects/<sanitized-git-root>/memory/) — per-user, per-project
  2. Team memory (<auto-mem-path>/team/) — shared across contributors (feature-gated)
  3. Memory scanning — reads frontmatter headers to build a manifest without loading full content
  4. Relevance selection — uses a Sonnet model call to pick the most relevant files (up to 5) for a given query
  5. Freshness warnings — age-based staleness caveats injected as <system-reminder> tags

Source directory: src/memdir/


2.2 Memory Directory Structure

~/.claude/
  projects/
    <sanitized-git-root>/
      memory/
        MEMORY.md           -- entrypoint index (always loaded)
        <topic-file>.md     -- individual memory files with frontmatter
        team/               -- team-shared memories (TEAMMEM feature)
          MEMORY.md
          <topic-file>.md
        logs/               -- KAIROS mode: append-only daily logs
          YYYY/
            MM/
              YYYY-MM-DD.md

2.3 Memory Types Taxonomy

All memory falls into four types (stored in frontmatter type: field):

Type Scope (combined mode) Description
user Always private User's role, goals, knowledge, preferences
feedback Private (default) or team for project-wide conventions How-to-approach-work guidance from corrections and confirmations
project Strongly bias team Ongoing work, goals, bugs, incidents not derivable from code
reference Usually team Pointers to external systems (Linear, Grafana, Slack)

What NOT to save:

  • Code patterns, architecture, file structure (derivable by reading code)
  • Git history, recent changes (use git log)
  • Debugging solutions/fix recipes (the fix is in the code)
  • Anything in CLAUDE.md
  • Ephemeral task details, in-progress work

2.4 Memory Frontmatter Format

---
name: {{memory name}}
description: {{one-line description — used to decide relevance in future conversations, so be specific}}
type: {{user, feedback, project, reference}}
---

{{memory content}}

For feedback and project types, the body should include:

  • Rule/fact as the lead
  • **Why:** line with the reason
  • **How to apply:** line with when/where it kicks in

2.5 Architecture: Data Flow

User query arrives
    │
    ▼
scanMemoryFiles(memoryDir)          -- reads frontmatter of all .md files
    │  returns MemoryHeader[]
    ▼
filterOut(alreadySurfaced)          -- skip files shown in prior turns
    │
    ▼
selectRelevantMemories(query, ...)  -- sideQuery to Sonnet with manifest
    │  returns up to 5 filenames
    ▼
Return RelevantMemory[] (path + mtimeMs)
    │
    ▼
Caller injects file contents        -- reads full file content
Caller adds freshness warnings      -- memoryFreshnessNote(mtimeMs)

2.6 Memory Scanning (memoryScan.ts)

scanMemoryFiles(memoryDir, signal)

  • readdir(memoryDir, { recursive: true }) to find all .md files
  • Excludes MEMORY.md (entrypoint — already in system prompt)
  • Reads only first FRONTMATTER_MAX_LINES = 30 lines of each file
  • Parses frontmatter for description and type
  • Sorts newest-first by mtimeMs
  • Capped at MAX_MEMORY_FILES = 200
  • Returns MemoryHeader[]

MemoryHeader type:

export type MemoryHeader = {
  filename: string
  filePath: string
  mtimeMs: number
  description: string | null
  type: MemoryType | undefined
}

formatMemoryManifest(memories) Formats headers as text manifest for the selector prompt:

- [user] user_role.md (2026-01-15T12:00:00Z): user is a senior Go engineer focused on observability
- [feedback] no_mocks.md (2026-01-10T09:30:00Z): integration tests must use real database

2.7 Relevance Selection (findRelevantMemories.ts)

System prompt used for selection:

You are selecting memories that will be useful to Claude Code as it processes a user's query.
Return a list of filenames for the memories that will clearly be useful (up to 5).
Only include memories you are certain will be helpful. Be selective and discerning.
- If unsure, do not include it.
- If no memories are clearly useful, return an empty list.
- If recently-used tools are provided, do not select reference docs for those tools
  (skip API usage docs, DO keep warnings/gotchas about active tools).

sideQuery parameters:

  • Model: getDefaultSonnetModel()
  • Max tokens: 256
  • Output format: JSON schema { selected_memories: string[] }
  • Query source: 'memdir_relevance'

findRelevantMemories signature:

export async function findRelevantMemories(
  query: string,
  memoryDir: string,
  signal: AbortSignal,
  recentTools: readonly string[] = [],
  alreadySurfaced: ReadonlySet<string> = new Set(),
): Promise<RelevantMemory[]>

export type RelevantMemory = {
  path: string
  mtimeMs: number
}

2.8 Memory Freshness System (memoryAge.ts)

All functions are pure, accepting mtimeMs timestamps:

Export Signature Description
memoryAgeDays(mtimeMs) (number) => number Floor-rounded days since mtime, clamped to ≥0
memoryAge(mtimeMs) (number) => string Human-readable: 'today', 'yesterday', 'N days ago'
memoryFreshnessText(mtimeMs) (number) => string Staleness caveat text ('' for ≤1 day, warns for older)
memoryFreshnessNote(mtimeMs) (number) => string <system-reminder> wrapped freshness text

Staleness warning text (for memories >1 day old):

This memory is N days old. Memories are point-in-time observations, not live state —
claims about code behavior or file:line citations may be outdated.
Verify against current code before asserting as fact.

2.9 Memory Path Resolution (paths.ts)

isAutoMemoryEnabled() — Priority chain:

  1. CLAUDE_CODE_DISABLE_AUTO_MEMORY env var (truthy → OFF, falsy-defined → ON)
  2. CLAUDE_CODE_SIMPLE (--bare mode) → OFF
  3. CLAUDE_CODE_REMOTE without CLAUDE_CODE_REMOTE_MEMORY_DIR → OFF
  4. settings.autoMemoryEnabled (project-level opt-out)
  5. Default: enabled

getAutoMemPath() — Resolution order (memoized by getProjectRoot()):

  1. CLAUDE_COWORK_MEMORY_PATH_OVERRIDE env var (Cowork spaces)
  2. autoMemoryDirectory in settings (policy > flag > local > user sources; supports ~/ expansion)
  3. <memoryBase>/projects/<sanitized-git-root>/memory/
    • memoryBase = CLAUDE_CODE_REMOTE_MEMORY_DIR or ~/.claude

Security validations in validateMemoryPath():

  • Rejects relative paths (not isAbsolute)
  • Rejects root/near-root (length < 3)
  • Rejects Windows drive-root (C:)
  • Rejects UNC paths (\\server\share, //)
  • Rejects null bytes
  • Normalizes NFC

getAutoMemDailyLogPath(date?) — KAIROS mode daily log path:

<autoMemPath>/logs/YYYY/MM/YYYY-MM-DD.md

All path exports:

Export Description
isAutoMemoryEnabled() Check if auto-memory is on
isExtractModeActive() Whether background memory extraction agent will run
getMemoryBaseDir() Base dir: env override or ~/.claude
getAutoMemPath() Full memory directory path (memoized)
getAutoMemDailyLogPath(date?) Daily log path for KAIROS mode
getAutoMemEntrypoint() MEMORY.md path inside auto-mem dir
isAutoMemPath(absolutePath) Check if path is inside auto-memory dir
hasAutoMemPathOverride() Check if Cowork env override is active

2.10 Team Memory Paths (teamMemPaths.ts)

Team memory lives at <autoMemPath>/team/. Heavy path traversal protection:

PathTraversalError — Custom error class thrown on injection attempts.

sanitizePathKey(key) — Rejects:

  • Null bytes
  • URL-encoded traversals (e.g. %2e%2e%2f)
  • Unicode normalization attacks (fullwidth ../)
  • Backslashes
  • Absolute paths

validateTeamMemWritePath(filePath) — Two-pass validation:

  1. path.resolve() to eliminate .. segments, check string containment
  2. realpathDeepestExisting() to follow symlinks and verify real containment

realpathDeepestExisting(absolutePath) — Walks up directory tree until realpath() succeeds, handles dangling symlinks (detects via lstat) and symlink loops (ELOOP).

All exports:

Export Description
PathTraversalError Error class for traversal attempts
isTeamMemoryEnabled() GrowthBook gate tengu_herring_clock + auto-memory enabled
getTeamMemPath() <autoMemPath>/team/
getTeamMemEntrypoint() <autoMemPath>/team/MEMORY.md
isTeamMemPath(filePath) String-level containment check
isTeamMemFile(filePath) Team enabled + path contained
validateTeamMemWritePath(filePath) Full symlink-safe write validation
validateTeamMemKey(relativeKey) Sanitize relative key + validate write path

2.11 Memory Prompt Building (memdir.ts)

Constants:

export const ENTRYPOINT_NAME = 'MEMORY.md'
export const MAX_ENTRYPOINT_LINES = 200
export const MAX_ENTRYPOINT_BYTES = 25_000

truncateEntrypointContent(raw) — Line-truncates first, then byte-truncates at last newline before cap. Appends warning message naming which cap(s) fired.

buildMemoryLines(displayName, memoryDir, extraGuidelines?, skipIndex?) — Builds behavioral instructions without MEMORY.md content. Used in system prompt.

buildMemoryPrompt({ displayName, memoryDir, extraGuidelines? }) — Same as buildMemoryLines but includes MEMORY.md content. Used by agent memory.

buildSearchingPastContextSection(autoMemDir) — Conditionally adds a "Searching past context" section with grep commands (gated on tengu_coral_fern GrowthBook feature).

ensureMemoryDirExists(memoryDir) — Idempotent mkdir (recursive); logs non-EEXIST errors without throwing.

loadMemoryPrompt() — Top-level dispatcher:

  • KAIROS + kairosActive → buildAssistantDailyLogPrompt()
  • TEAMMEM + team enabled → buildCombinedMemoryPrompt()
  • Auto enabled → buildMemoryLines() joined
  • Otherwise → null

2.12 Memory Type Constants (memoryTypes.ts)

All exports used in system prompt construction:

Export Type Description
MEMORY_TYPES readonly string[] ['user', 'feedback', 'project', 'reference']
MemoryType type Union of the four type strings
parseMemoryType(raw) (unknown) => MemoryType | undefined Parses frontmatter value
TYPES_SECTION_COMBINED readonly string[] Prompt section for dual-directory mode
TYPES_SECTION_INDIVIDUAL readonly string[] Prompt section for single-directory mode
WHAT_NOT_TO_SAVE_SECTION readonly string[] Exclusion list for memory content
MEMORY_DRIFT_CAVEAT string Single bullet warning about memory staleness
WHEN_TO_ACCESS_SECTION readonly string[] When to read memories
TRUSTING_RECALL_SECTION readonly string[] Verification guidance before recommending from memory
MEMORY_FRONTMATTER_EXAMPLE readonly string[] Example frontmatter block for the prompt

2.13 Team Memory Prompts (teamMemPrompts.ts)

buildCombinedMemoryPrompt(extraGuidelines?, skipIndex?) — Builds the combined prompt when both auto and team memory are enabled. Includes:

  • Both directory paths
  • Two-scope explanation (private vs team)
  • Combined TYPES_SECTION_COMBINED with <scope> tags
  • Dual MEMORY.md index instructions

3. Keybindings System

3.1 System Overview

The keybindings system provides a fully configurable keyboard shortcut layer for Claude Code. Users can customize bindings via ~/.claude/keybindings.json (gated on tengu_keybinding_customization_release GrowthBook feature). The system supports:

  • Single-keystroke bindings
  • Multi-keystroke chords (e.g., ctrl+x ctrl+k)
  • Context-scoped bindings (Chat, Global, Confirmation, etc.)
  • Null-unbinding (set action to null to disable a default)
  • Command bindings (command:help executes slash commands)
  • Hot-reload via chokidar file watcher

Source directory: src/keybindings/


3.2 Architecture

DEFAULT_BINDINGS (KeybindingBlock[])
    │
    ▼ parseBindings()
ParsedBinding[]   <── default bindings parsed at startup
    │
    ▼ merge with user bindings (user appended after, so user wins)
ParsedBinding[]   <── merged bindings (last-wins for same key+context)
    │
    ▼
KeybindingProvider (React context)
    │
    ├── resolveKeyWithChordState()  ◄── called on every keypress
    │   ├── chord_started → pending state
    │   ├── chord_cancelled → reset
    │   ├── match → invoke action
    │   ├── unbound → swallow event
    │   └── none → pass through
    │
    └── useKeybinding(action, context, handler)  ◄── component hook

3.3 Keybinding Contexts

18 contexts defined in KEYBINDING_CONTEXTS:

Context Activation
Global Active everywhere, regardless of focus
Chat When the chat input is focused
Autocomplete When autocomplete menu is visible
Confirmation When a confirmation/permission dialog is shown
Help When the help overlay is open
Transcript When viewing the transcript
HistorySearch When searching command history (ctrl+r)
Task When a task/agent is running in the foreground
ThemePicker When the theme picker is open
Settings When the settings menu is open
Tabs When tab navigation is active
Attachments When navigating image attachments in a select dialog
Footer When footer indicators are focused
MessageSelector When the message selector (rewind) is open
DiffDialog When the diff dialog is open
ModelPicker When the model picker is open
Select When a select/list component is focused
Plugin When the plugin dialog is open

3.4 Default Bindings (defaultBindings.ts)

Platform-specific keys:

  • IMAGE_PASTE_KEY: alt+v (Windows), ctrl+v (others)
  • MODE_CYCLE_KEY: meta+m (Windows without VT mode), shift+tab (others)
  • VT mode support check: Node ≥22.17.0 or ≥24.2.0, Bun ≥1.2.23

Complete default bindings by context:

Global context:

Key Action
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+shift+b app:toggleBrief (KAIROS/KAIROS_BRIEF only)
ctrl+shift+o app:toggleTeammatePreview
ctrl+r history:search
ctrl+shift+f / cmd+shift+f app:globalSearch (QUICK_SEARCH feature)
ctrl+shift+p / cmd+shift+p app:quickOpen (QUICK_SEARCH feature)
meta+j app:toggleTerminal (TERMINAL_PANEL feature)

Chat context:

Key Action
escape chat:cancel
ctrl+x ctrl+k chat:killAgents (chord)
shift+tab / meta+m chat:cycleMode
meta+p chat:modelPicker
meta+o chat:fastMode
meta+t chat:thinkingToggle
enter chat:submit
up history:previous
down history:next
ctrl+_ / ctrl+shift+- chat:undo
ctrl+x ctrl+e / ctrl+g chat:externalEditor
ctrl+s chat:stash
ctrl+v / alt+v chat:imagePaste
shift+up chat:messageActions (MESSAGE_ACTIONS feature)
space voice:pushToTalk (VOICE_MODE feature)

Autocomplete:

Key Action
tab autocomplete:accept
escape autocomplete:dismiss
up / down autocomplete:previous / autocomplete:next

Confirmation:

Key Action
y / enter confirm:yes
n / escape confirm:no
up / down confirm:previous / confirm:next
tab confirm:nextField
space confirm:toggle
shift+tab confirm:cycleMode
ctrl+e confirm:toggleExplanation
ctrl+d permission:toggleDebug

Scroll:

Key Action
pageup / pagedown scroll:pageUp / scroll:pageDown
wheelup / wheeldown scroll:lineUp / scroll:lineDown
ctrl+home / ctrl+end scroll:top / scroll:bottom
ctrl+shift+c / cmd+c selection:copy

Task:

Key Action
ctrl+b task:background

3.5 Parser (parser.ts)

parseKeystroke(input: string): ParsedKeystroke Splits on +, recognizes modifiers (case-insensitive aliases):

  • ctrl / control
  • alt / opt / option
  • shift
  • meta
  • cmd / command / super / win
  • Special keys: escescape, returnenter, space , ↑↓←→

parseChord(input: string): Chord Splits space-separated steps into ParsedKeystroke[]. Special case: lone space " " is the space key, not a separator.

keystrokeToString(ks: ParsedKeystroke): string Canonical string representation (for internal use).

keystrokeToDisplayString(ks, platform?): string Platform-appropriate display: opt on macOS, alt elsewhere; cmd on macOS, super elsewhere.

parseBindings(blocks: KeybindingBlock[]): ParsedBinding[] Converts KeybindingBlock[] (raw config) to flat ParsedBinding[].


3.6 Matching (match.ts)

getKeyName(input: string, key: Key): string | null Maps Ink's boolean key flags to string names (key.escape'escape', key.upArrow'up', single char → lowercased, etc.).

matchesKeystroke(input, key, target): boolean

  • Extracts key name and modifiers from Ink's Key object
  • Quirk: key.meta = true when escape is pressed in Ink (legacy terminal behavior). Ignored for escape key matching itself.
  • Alt and meta are treated as aliases in Ink (terminal limitation): target.alt || target.meta matched against key.meta.
  • Super/cmd is distinct — only arrives via kitty keyboard protocol.

matchesBinding(input, key, binding): boolean Single-keystroke binding match only (chord length must be 1).


3.7 Resolver (resolver.ts)

resolveKey(input, key, activeContexts, bindings): ResolveResult Pure function for single-keystroke resolution:

  • Last matching binding wins (for user overrides)
  • Returns { type: 'match', action }, { type: 'none' }, or { type: 'unbound' }

resolveKeyWithChordState(input, key, activeContexts, bindings, pending): ChordResolveResult Multi-keystroke chord resolution with state:

  • Cancels on escape when in a chord
  • Groups chord candidates by chordToString(chord) key to handle null-unbinding properly (a null override on a chord prevents its prefix from entering chord-wait)
  • Returns additional types: { type: 'chord_started', pending }, { type: 'chord_cancelled' }

ChordResolveResult:

type ChordResolveResult =
  | { type: 'match'; action: string }
  | { type: 'none' }
  | { type: 'unbound' }
  | { type: 'chord_started'; pending: ParsedKeystroke[] }
  | { type: 'chord_cancelled' }

getBindingDisplayText(action, context, bindings): string | undefined Searches bindings in reverse order (last wins) for the given action+context.

keystrokesEqual(a, b): boolean Compares with alt/meta collapsed into one logical modifier.


3.8 Type Definitions (types.ts — inferred)

type KeybindingContextName = (typeof KEYBINDING_CONTEXTS)[number]

type ParsedKeystroke = {
  key: string
  ctrl: boolean
  alt: boolean
  shift: boolean
  meta: boolean
  super: boolean
}

type Chord = ParsedKeystroke[]

type ParsedBinding = {
  chord: Chord
  action: string | null  // null = unbound
  context: KeybindingContextName
}

type KeybindingBlock = {
  context: string
  bindings: Record<string, string | null>
}

3.9 Schema (schema.ts)

Zod v4 schemas for keybindings.json validation:

KeybindingBlockSchema — Validates { context, bindings: { key: action | null } }. Bindings accept:

  • Known action strings from KEYBINDING_ACTIONS
  • command: prefix bindings (/^command:[a-zA-Z0-9:\-_]+$/)
  • null (to unbind)

KeybindingsSchema — Wraps with optional $schema and $docs metadata:

{
  "$schema": "https://www.schemastore.org/claude-code-keybindings.json",
  "$docs": "https://code.claude.com/docs/en/keybindings",
  "bindings": [...]
}

Complete KEYBINDING_ACTIONS list (80 actions): Categories: app:*, history:*, chat:*, autocomplete:*, confirm:*, tabs:*, transcript:*, historySearch:*, task:*, theme:*, help:*, attachments:*, footer:*, messageSelector:*, messageActions:*, diff:*, modelPicker:*, select:*, plugin:*, permission:*, settings:*, voice:*


3.10 Validation (validate.ts)

KeybindingWarningType: 'parse_error' | 'duplicate' | 'reserved' | 'invalid_context' | 'invalid_action'

KeybindingWarning:

type KeybindingWarning = {
  type: KeybindingWarningType
  severity: 'error' | 'warning'
  message: string
  key?: string
  context?: string
  action?: string
  suggestion?: string
}

Validation checks:

  1. validateUserConfig(blocks) — validates block structure (context string, bindings object)
  2. checkDuplicates(blocks) — within-context duplicate keys (normalized comparison)
  3. checkReservedShortcuts(bindings) — against NON_REBINDABLE and platform TERMINAL_RESERVED/MACOS_RESERVED
  4. checkDuplicateKeysInJson(jsonString) — raw JSON string scan (JSON.parse silently drops earlier duplicates)
  5. Special case: voice:pushToTalk with bare alphabetic key warns (prints into input during warmup)
  6. command: bindings must be in Chat context

3.11 Reserved Shortcuts (reservedShortcuts.ts)

NON_REBINDABLE (error severity):

  • ctrl+c — interrupt/exit (hardcoded double-press logic)
  • ctrl+d — exit (hardcoded)
  • ctrl+m — identical to Enter in terminals (both send CR)

TERMINAL_RESERVED:

  • ctrl+z — Unix SIGTSTP (warning)
  • ctrl+\ — terminal SIGQUIT (error)

MACOS_RESERVED (macOS only, all errors):

  • cmd+c/v/x/q/w/tab/space

Note: ctrl+s (XOFF) and ctrl+q (XON) are intentionally NOT in reserved list — modern terminals disable flow control and Claude Code uses ctrl+s for stash.


3.12 User Bindings Loader (loadUserBindings.ts)

File location: ~/.claude/keybindings.json

isKeybindingCustomizationEnabled() — GrowthBook gate: tengu_keybinding_customization_release.

File watching: Uses chokidar with:

  • stabilityThreshold: 500ms — waits for write to stabilize
  • pollInterval: 200ms
  • atomic: true
  • Watches parent directory, not just the file (handles file creation)

loadKeybindings() / loadKeybindingsSyncWithWarnings() — Load+merge+validate. Returns { bindings: ParsedBinding[], warnings: KeybindingWarning[] }.

All exports:

Export Description
isKeybindingCustomizationEnabled() GrowthBook gate check
getKeybindingsPath() ~/.claude/keybindings.json
loadKeybindings() Async load (used in watcher)
loadKeybindingsSync() Sync load (React useState initializer)
loadKeybindingsSyncWithWarnings() Sync with warnings
initializeKeybindingWatcher() Set up chokidar watcher
disposeKeybindingWatcher() Tear down watcher
subscribeToKeybindingChanges Subscribe to reload events
getCachedKeybindingWarnings() Current cached warnings
resetKeybindingLoaderForTesting() Test reset

3.13 Template Generator (template.ts)

generateKeybindingsTemplate() — Generates a well-documented template JSON file with all default bindings. Filters out NON_REBINDABLE shortcuts to avoid /doctor warnings. Includes $schema and $docs metadata.


3.14 Shortcut Display (shortcutFormat.ts)

getShortcutDisplay(action, context, fallback) — Non-React shortcut display helper. Logs tengu_keybinding_fallback_used analytics event (once per action+context) when the binding is not found. Falls back to hardcoded string during migration.


4. Skills System

4.1 System Overview

Skills are Claude's slash commands that execute AI prompts or local logic. They come from multiple sources:

  • Bundled skills — compiled into the binary, available to all users
  • Disk-based skills — markdown files in .claude/skills/ directories
  • Plugin skills — provided by installed plugins
  • MCP skills — generated from MCP server tool definitions

Source directory: src/skills/


4.2 Bundled Skills Registry (bundledSkills.ts)

BundledSkillDefinition type:

type BundledSkillDefinition = {
  name: string
  description: string
  aliases?: string[]
  whenToUse?: string
  argumentHint?: string
  allowedTools?: string[]
  model?: string
  disableModelInvocation?: boolean
  userInvocable?: boolean
  isEnabled?: () => boolean
  hooks?: HooksSettings
  context?: 'inline' | 'fork'  // fork = run as sub-agent
  agent?: string
  files?: Record<string, string>  // reference files extracted to disk on first use
  getPromptForCommand: (args: string, context: ToolUseContext) => Promise<ContentBlockParam[]>
}

File extraction (files field): When a skill has files, they are extracted to a per-process nonce directory on first invocation using O_NOFOLLOW | O_EXCL | O_CREAT flags (mode 0o600, directories mode 0o700). A Base directory for this skill: <dir> prefix is prepended to the skill prompt so the model can read these files.

Registry API:

Export Description
registerBundledSkill(definition) Register a bundled skill at startup
getBundledSkills() Get copy of all registered bundled skills
clearBundledSkills() Clear registry (for testing)
getBundledSkillExtractDir(skillName) Deterministic extract directory

4.3 Bundled Skill Initialization (bundled/index.ts)

initBundledSkills() registers the following skills:

Skill Feature Gate Description
updateConfig Update Claude Code configuration
keybindings Manage keyboard shortcuts
verify Verify work / run checks
debug Debug assistance
loremIpsum Lorem ipsum text generation
skillify Create new skills
remember Explicitly save to memory
simplify Simplify code/text
batch Batch operations
stuck Help when stuck
dream KAIROS | KAIROS_DREAM Nightly memory distillation
hunter REVIEW_ARTIFACT Code review
loop AGENT_TRIGGERS Recurring agent triggers
scheduleRemoteAgents AGENT_TRIGGERS_REMOTE Schedule remote agent runs
claudeApi BUILDING_CLAUDE_APPS Claude API reference skill
claudeInChrome shouldAutoEnableClaudeInChrome() Claude in Chrome integration
runSkillGenerator RUN_SKILL_GENERATOR Skill generation tool

4.4 Skills Directory Loader (loadSkillsDir.ts)

LoadedFrom type:

type LoadedFrom =
  | 'commands_DEPRECATED'
  | 'skills'
  | 'plugin'
  | 'managed'
  | 'bundled'
  | 'mcp'

getSkillsPath(source, dir) — Returns the config directory path for a given source and subdirectory (skills or commands).

The loader handles:

  • Frontmatter parsing (name, description, argument-hint, when-to-use, allowed-tools, model, disableNonInteractive, hooks, context, agent, effort, paths)
  • Shell execution in prompts (backtick commands in frontmatter)
  • Gitignore integration
  • Argument substitution ($ARGUMENTS, named args $ARG_NAME)
  • isRestrictedToPluginOnly() policy check
  • Multi-level discovery: project dirs up to home + user config

5. Voice System

5.1 System Overview

Voice mode enables hold-to-talk interaction with Claude Code using a push-to-talk model. Voice requires:

  1. OAuth authentication (uses voice_stream endpoint on claude.ai)
  2. GrowthBook feature flag VOICE_MODE
  3. Kill-switch gate: tengu_amber_quartz_disabled (negative flag)

Source: src/voice/voiceModeEnabled.ts


5.2 API

Export Description
isVoiceGrowthBookEnabled() feature('VOICE_MODE') && !tengu_amber_quartz_disabled
hasVoiceAuth() OAuth provider + valid access token exists
isVoiceModeEnabled() Full check: hasVoiceAuth() && isVoiceGrowthBookEnabled()

Why OAuth required:

  • Voice uses voice_stream endpoint on claude.ai
  • Not available with API keys, Bedrock, Vertex, or Foundry
  • isAnthropicAuthEnabled() checks the provider, getClaudeAIOAuthTokens() verifies a token actually exists

Kill-switch design:

  • Flag default is false = not killed
  • A missing/stale disk cache reads as "not killed" → voice works immediately on fresh installs
  • Emergency kill: flip tengu_amber_quartz_disabled to true

Default binding: spacevoice:pushToTalk (defined in defaultBindings.ts under VOICE_MODE feature gate)

Validation warning: Binding voice:pushToTalk to a bare alphabetic key (no modifiers) raises a warning because the key prints into the input during the activation warmup period. Recommended: space or a modifier combo like meta+k.


6. Plugins System

6.1 System Overview

Plugins extend Claude Code with additional skills, hooks, MCP servers, LSP servers, and output styles. There are two plugin categories:

  1. Built-in plugins — ship with the CLI, appear in /plugin UI, user-toggleable
  2. Marketplace plugins — installed from GitHub repos via /plugin install

Source: src/plugins/


6.2 Built-in Plugin Registry (builtinPlugins.ts)

Built-in plugins use the ID format {name}@builtin.

BuiltinPluginDefinition type (from types/plugin.ts):

type BuiltinPluginDefinition = {
  name: string
  description: string
  version?: string
  skills?: BundledSkillDefinition[]
  hooks?: HooksSettings
  mcpServers?: Record<string, McpServerConfig>
  isAvailable?: () => boolean
  defaultEnabled?: boolean  // defaults to true
}

LoadedPlugin type:

type LoadedPlugin = {
  name: string
  manifest: PluginManifest
  path: string
  source: string
  repository: string
  enabled?: boolean
  isBuiltin?: boolean
  sha?: string
  commandsPath?: string
  commandsPaths?: string[]
  commandsMetadata?: Record<string, CommandMetadata>
  agentsPath?: string
  agentsPaths?: string[]
  skillsPath?: string
  skillsPaths?: string[]
  outputStylesPath?: string
  outputStylesPaths?: string[]
  hooksConfig?: HooksSettings
  mcpServers?: Record<string, McpServerConfig>
  lspServers?: Record<string, LspServerConfig>
  settings?: Record<string, unknown>
}

getBuiltinPlugins() — Returns { enabled: LoadedPlugin[], disabled: LoadedPlugin[] }. Enabled state priority: user setting > defaultEnabled > true. Plugins with isAvailable() === false are omitted entirely.

All exports:

Export Description
BUILTIN_MARKETPLACE_NAME 'builtin' — sentinel marketplace name
registerBuiltinPlugin(definition) Register at startup
isBuiltinPluginId(pluginId) Check if ends with @builtin
getBuiltinPluginDefinition(name) Get definition by name
getBuiltinPlugins() Get enabled/disabled split
getBuiltinPluginSkillCommands() Get skills from enabled plugins as Commands
clearBuiltinPlugins() Test reset

6.3 Built-in Plugin Initialization (bundled/index.ts)

export function initBuiltinPlugins(): void {
  // No built-in plugins registered yet — scaffolding for migrating
  // bundled skills that should be user-toggleable.
}

The file is scaffolding — as of the code snapshot, no built-in plugins have been registered. The infrastructure is complete for migrating bundled skills into the toggleable plugin system.


6.4 Plugin Error Types (types/plugin.ts)

PluginError is a large discriminated union with 20+ error variants:

Type Key fields
path-not-found path, component
git-auth-failed gitUrl, authType: 'ssh' | 'https'
git-timeout gitUrl, operation: 'clone' | 'pull'
network-error url, details?
manifest-parse-error manifestPath, parseError
manifest-validation-error manifestPath, validationErrors[]
plugin-not-found pluginId, marketplace
marketplace-not-found marketplace, availableMarketplaces[]
marketplace-load-failed marketplace, reason
mcp-config-invalid serverName, validationError
mcp-server-suppressed-duplicate serverName, duplicateOf
lsp-config-invalid serverName, validationError
lsp-server-start-failed serverName, reason
lsp-server-crashed exitCode, signal?
lsp-request-timeout method, timeoutMs
lsp-request-failed method, error
marketplace-blocked-by-policy marketplace, blockedByBlocklist?, allowedSources[]
dependency-unsatisfied dependency, reason: 'not-enabled' | 'not-found'
plugin-cache-miss installPath
hook-load-failed hookPath, reason
component-load-failed component, path, reason
mcpb-download-failed url, reason
mcpb-extract-failed mcpbPath, reason
mcpb-invalid-manifest mcpbPath, validationError
generic-error error

getPluginErrorMessage(error) — Returns a human-readable string for any PluginError variant.

PluginLoadResult:

type PluginLoadResult = {
  enabled: LoadedPlugin[]
  disabled: LoadedPlugin[]
  errors: PluginError[]
}

7. Output Styles

7.1 System Overview

Output styles are markdown files that define custom formatting instructions for Claude's responses. Loaded from output-styles/ subdirectories in project .claude/ and user ~/.claude/ directories.

Source: src/outputStyles/loadOutputStylesDir.ts


7.2 Configuration

  • Location: .claude/output-styles/*.md (project) and ~/.claude/output-styles/*.md (user)
  • File naming: filename.md → style name filename
  • Frontmatter fields:
    • name: Override the style name (defaults to filename without .md)
    • description: Description shown in output style picker
    • keep-coding-instructions: Boolean — whether to preserve coding-specific instructions (true/false or boolean)
    • force-for-plugin: Only valid for plugin output styles; ignored (with warning) on regular styles

7.3 API

getOutputStyleDirStyles(cwd: string): Promise<OutputStyleConfig[]> — Memoized async loader. Scans output-styles subdirectory across project hierarchy and user config. Returns OutputStyleConfig[].

OutputStyleConfig (from constants/outputStyles.ts):

type OutputStyleConfig = {
  name: string
  description: string
  prompt: string         // file content (trimmed)
  source: string         // setting source (userSettings, localSettings, etc.)
  keepCodingInstructions?: boolean
}

clearOutputStyleCaches() — Clears memoized getOutputStyleDirStyles, loadMarkdownFilesForSubdir, and plugin output style caches.


8. Hooks Schema

8.1 System Overview

Hooks execute side effects at specific lifecycle events (PreToolUse, PostToolUse, PostResponse, etc.). The schema defines four hook types in a discriminated union, with conditional execution via if conditions.

Source: src/schemas/hooks.ts


8.2 Hook Types

BashCommandHook (type: 'command')

{
  type: 'command'
  command: string           // Shell command to execute
  if?: string               // Permission rule syntax filter (e.g., "Bash(git *)")
  shell?: 'bash' | 'powershell'
  timeout?: number          // Seconds
  statusMessage?: string    // Spinner message
  once?: boolean            // Run once, then remove
  async?: boolean           // Non-blocking background execution
  asyncRewake?: boolean     // Background + wake model on exit code 2
}

PromptHook (type: 'prompt')

{
  type: 'prompt'
  prompt: string            // LLM prompt (use $ARGUMENTS for hook input JSON)
  if?: string
  timeout?: number
  model?: string            // Default: small fast model
  statusMessage?: string
  once?: boolean
}

HttpHook (type: 'http')

{
  type: 'http'
  url: string               // POST destination
  if?: string
  timeout?: number
  headers?: Record<string, string>  // May use $VAR_NAME interpolation
  allowedEnvVars?: string[]          // Explicit allowlist for env var interpolation
  statusMessage?: string
  once?: boolean
}

AgentHook (type: 'agent')

{
  type: 'agent'
  prompt: string            // Verification prompt (use $ARGUMENTS for hook input JSON)
  if?: string
  timeout?: number          // Default: 60 seconds
  model?: string            // Default: Haiku
  statusMessage?: string
  once?: boolean
}

Note on AgentHook: Must NOT use .transform() in the Zod schema — the schema is used in parseSettingsFile, and round-tripping a transformed function through JSON.stringify silently drops the prompt field.


8.3 Hook Matcher and Settings Structure

type HookMatcher = {
  matcher?: string          // e.g., "Write", "Bash(git *)"
  hooks: HookCommand[]
}

type HooksSettings = Partial<Record<HookEvent, HookMatcher[]>>

IfConditionSchema: Shared if field uses permission rule syntax (Bash(git *), Read(*.ts)) to filter hook execution before spawning. Evaluated against tool_name and tool_input.


8.4 Exports

Export Description
HookCommandSchema Discriminated union of all 4 hook command types
HookMatcherSchema { matcher?, hooks[] }
HooksSchema Partial<Record<HookEvent, HookMatcher[]>>
HookCommand Inferred type from schema
BashCommandHook Extract<HookCommand, { type: 'command' }>
PromptHook Extract<HookCommand, { type: 'prompt' }>
AgentHook Extract<HookCommand, { type: 'agent' }>
HttpHook Extract<HookCommand, { type: 'http' }>
HookMatcher Inferred from matcher schema
HooksSettings Partial<Record<HookEvent, HookMatcher[]>>

9. Native-TypeScript Ports (native-ts/)

The native-ts/ directory contains pure-TypeScript ports of Rust NAPI native modules. These are used when the native modules cannot be loaded (platforms without precompiled binaries, etc.).

9.1 Color Diff (native-ts/color-diff/index.ts)

Purpose: Port of vendor/color-diff-src — syntax-highlighted word-level diff rendering for the diff view.

Implementation: Uses highlight.js (lazy-loaded to defer ~50MB grammar registration) and the diff npm package's diffArrays.

Semantic differences from native:

  • Syntax highlighting via highlight.js instead of syntect/bat
  • Scope colors approximate syntect output, but plain identifiers and = : operators render in default fg (no scope in hljs grammar)
  • BAT_THEME env is a stub (always returns default theme)
  • Output structure (line numbers, markers, backgrounds, word-diff) is identical

Lazy loading pattern:

let cachedHljs: HLJSApi | null = null
function hljs(): HLJSApi {
  if (cachedHljs) return cachedHljs
  const mod = require('highlight.js')
  // Handles both ESM default export and CJS module-is-API interop
  cachedHljs = 'default' in mod && mod.default ? mod.default : mod
  return cachedHljs!
}

Key types:

type Hunk = {
  oldStart: number
  oldLines: number
  newStart: number
  newLines: number
  lines: string[]
}

type SyntaxTheme = { ... }

9.2 File Index (native-ts/file-index/index.ts)

Purpose: Port of vendor/file-index-src — high-performance fuzzy file path search, replacing nucleo (Rust).

Algorithm: Approximates fzf-v2/nucleo scoring with:

  • Bitmap reject: paths missing any needle letter rejected in O(1)
  • Greedy-earliest position scan via indexOf (SIMD-accelerated in JSC/V8)
  • Scoring constants:
Constant Value Description
SCORE_MATCH 16 Base score per matched char
BONUS_BOUNDARY 8 Bonus for path boundary match (/ \ - _ . space)
BONUS_CAMEL 6 Bonus for camelCase boundary
BONUS_CONSECUTIVE 4 Bonus for consecutive matches
BONUS_FIRST_CHAR 8 Bonus for matching first char
PENALTY_GAP_START 3 Penalty for starting a gap
PENALTY_GAP_EXTENSION 1 Penalty per gap character

Test file penalty: Paths containing "test" get a 1.05× score penalty (capped at 1.0).

Score semantics: Lower = better. score = position_in_results / result_count, so best match is 0.0.

Smart case: Lowercase query → case-insensitive; any uppercase → case-sensitive.

FileIndex class:

class FileIndex {
  loadFromFileList(fileList: string[]): void
  loadFromFileListAsync(fileList: string[]): { queryable: Promise<void>; done: Promise<void> }
  search(query: string, limit: number): SearchResult[]
}

type SearchResult = {
  path: string
  score: number
}

Async loading: loadFromFileListAsync yields to event loop every CHUNK_MS = 4ms of sync work. Resolves queryable after first chunk (partial results immediately searchable). For a 270k-path list: ~510ms to first queryable state.

Top-level cache: Stores 100 most-common top-level path segments for empty-query results.

TOP_LEVEL_CACHE_LIMIT = 100, MAX_QUERY_LEN = 64


9.3 Yoga Layout (native-ts/yoga-layout/)

Purpose: Pure-TypeScript port of Meta's Yoga flexbox engine (yoga-layout/load), used by Ink's layout engine.

enums.ts — Yoga constants as const objects:

Enum Values
Align Auto(0), FlexStart(1), Center(2), FlexEnd(3), Stretch(4), Baseline(5), SpaceBetween(6), SpaceAround(7), SpaceEvenly(8)
BoxSizing BorderBox(0), ContentBox(1)
Dimension Width(0), Height(1)
Direction Inherit(0), LTR(1), RTL(2)
Display Flex(0), None(1), Contents(2)
Edge Left(0), Top(1), Right(2), Bottom(3), ...
Errata None(0), StretchFlexBasis(1), All(2147483647)
ExperimentalFeature WebFlexBasis(0)
FlexDirection Column(0), ColumnReverse(1), Row(2), RowReverse(3)
Gutter Column(0), Row(1), All(2)
Justify FlexStart(0), Center(1), FlexEnd(2), SpaceBetween(3), SpaceAround(4), SpaceEvenly(5)
MeasureMode Undefined(0), Exactly(1), AtMost(2)
NodeType Default(0), Text(1)
Overflow Visible(0), Hidden(1), Scroll(2)
PositionType Static(0), Relative(1), Absolute(2)
Unit Undefined(0), Point(1), Percent(2), Auto(3)
Wrap NoWrap(0), Wrap(1), WrapReverse(2)

index.ts — Subset of Yoga implemented:

  • flex-direction (row/column + reverse)
  • flex-grow / flex-shrink / flex-basis
  • align-items / align-self (all except baseline in practice)
  • justify-content (all 6 values)
  • margin / padding / border / gap
  • width / height / min / max (point, percent, auto)
  • position: relative / absolute
  • display: flex / none / contents
  • measure functions (text nodes)
  • margin: auto
  • flex-wrap + align-content
  • baseline alignment (spec parity, not used by Ink)

Not implemented: aspect-ratio, box-sizing: content-box, RTL direction


10. MoreRight Hook (moreright/)

10.1 System Overview

useMoreRight is a React hook that exists as a stub for external builds. The real implementation is internal-only (ant users). The stub returns no-op implementations of the hook interface.

Source: src/moreright/useMoreRight.tsx


10.2 Interface

export function useMoreRight(_args: {
  enabled: boolean
  setMessages: (action: M[] | ((prev: M[]) => M[])) => void
  inputValue: string
  setInputValue: (s: string) => void
  setToolJSX: (args: M) => void
}): {
  onBeforeQuery: (input: string, all: M[], n: number) => Promise<boolean>
  onTurnComplete: (all: M[], aborted: boolean) => Promise<void>
  render: () => null
}

Stub behavior:

  • onBeforeQuery always returns true (allow query)
  • onTurnComplete is a no-op
  • render returns null

The real internal implementation provides pre-query preprocessing and post-turn processing functionality whose details are redacted from the external build.


11. Migrations

Migrations run at startup to update stored config/settings to current formats. All migrations are idempotent (safe to re-run).

11.1 Migration Overview

Migration Source Target Condition
migrateAutoUpdatesToSettings globalConfig.autoUpdates: false userSettings.env.DISABLE_AUTOUPDATER: '1' Only when user-preference disabled (not native protection)
migrateBypassPermissionsAcceptedToSettings globalConfig.bypassPermissionsModeAccepted userSettings.skipDangerousModePermissionPrompt: true globalConfig field present
migrateEnableAllProjectMcpServersToSettings projectConfig.enableAllProjectMcpServers etc. localSettings Any of the 3 fields present
migrateFennecToOpus userSettings.model fennec aliases Opus 4.6 aliases ant users only
migrateLegacyOpusToCurrent Explicit Opus 4.0/4.1 model strings 'opus' alias 1P provider + legacy remap enabled
migrateOpusToOpus1m userSettings.model: 'opus' 'opus[1m]' isOpus1mMergeEnabled()
migrateReplBridgeEnabledToRemoteControlAtStartup globalConfig.replBridgeEnabled globalConfig.remoteControlAtStartup Old key exists, new key absent
migrateSonnet1mToSonnet45 userSettings.model: 'sonnet[1m]' 'sonnet-4-5-20250929[1m]' Once (sonnet1m45MigrationComplete flag)
migrateSonnet45ToSonnet46 Explicit Sonnet 4.5 strings 'sonnet' or 'sonnet[1m]' 1P + Pro/Max/TeamPremium
resetAutoModeOptInForDefaultOffer userSettings.skipAutoPermissionPrompt Clear the setting TRANSCRIPT_CLASSIFIER + specific conditions
resetProToOpusDefault Default model settings Sets migration timestamp 1P Pro users

11.2 Model Naming Evolution (revealed by migrations)

The migration history reveals the complete model codename/alias evolution:

Opus lineage:

  1. fennec-latest — internal codename for early Opus 4.x
  2. claude-opus-4-0 / claude-opus-4-20250514 — Opus 4.0 release
  3. claude-opus-4-1 / claude-opus-4-1-20250805 — Opus 4.1 release
  4. opus-4-5-fast — Opus 4.5 fast variant
  5. opus — Current Opus 4.6 alias
  6. opus[1m] — Opus 4.6 with 1M context window

Sonnet lineage:

  1. sonnet[1m] — Early Sonnet with 1M context (targeted Sonnet 4.5)
  2. claude-sonnet-4-5-20250929 / sonnet-4-5-20250929 — Sonnet 4.5 explicit
  3. claude-sonnet-4-5-20250929[1m] / sonnet-4-5-20250929[1m] — Sonnet 4.5 with 1M
  4. sonnet — Current Sonnet 4.6 alias
  5. sonnet[1m] — Sonnet 4.6 with 1M context (re-aliased)

Internal codenames:

  • fennec-latest → Opus 4.x series (pre-release internal name)
  • fennec-fast-latest → Opus fast variant

11.3 Migration Details

migrateAutoUpdatesToSettings

  • Only migrates when globalConfig.autoUpdates === false AND autoUpdatesProtectedForNative !== true
  • Adds DISABLE_AUTOUPDATER: '1' to userSettings.env
  • Sets process.env.DISABLE_AUTOUPDATER = '1' immediately
  • Removes autoUpdates and autoUpdatesProtectedForNative from globalConfig

migrateFennecToOpus (ant-only)

Alias mapping:

  • fennec-latest[1m]opus[1m]
  • fennec-latestopus
  • fennec-fast-latest / opus-4-5-fastopus[1m] + fastMode: true

migrateLegacyOpusToCurrent (1P only)

Migrates: claude-opus-4-20250514, claude-opus-4-1-20250805, claude-opus-4-0, claude-opus-4-1'opus' Sets legacyOpusMigrationTimestamp for one-time notification.

migrateOpusToOpus1m

Migrates 'opus''opus[1m]' for eligible Max/Team Premium 1P users. Idempotent: only acts when userSettings.model === 'opus' exactly. If the migrated value equals the current default, clears model setting instead.

migrateSonnet1mToSonnet45

Run once (tracked by sonnet1m45MigrationComplete in globalConfig). Also migrates the in-memory mainLoopModelOverride if set.

migrateSonnet45ToSonnet46

Migrates:

  • claude-sonnet-4-5-20250929 / sonnet-4-5-20250929'sonnet'
  • claude-sonnet-4-5-20250929[1m] / sonnet-4-5-20250929[1m]'sonnet[1m]' Sets sonnet45To46MigrationTimestamp for notification (skipped for new users with numStartups <= 1).

migrateReplBridgeEnabledToRemoteControlAtStartup

Renames replBridgeEnabled (internal implementation detail) to remoteControlAtStartup (user-facing key). Only acts when old key exists and new key is absent.

resetAutoModeOptInForDefaultOffer

One-shot (guarded by hasResetAutoModeOptInForDefaultOffer flag). Clears skipAutoPermissionPrompt for users who accepted the old 2-option dialog but don't have auto as their default mode. Re-surfaces the new dialog with the "make it my default" option. Only runs when getAutoModeEnabledState() === 'enabled' (not 'opt-in').

resetProToOpusDefault

Sets migration timestamp for Pro subscribers on 1P with no custom model setting (UI notification that Opus is now the default). For all other users, simply marks migration complete.


12. Core Type Definitions (types/)

12.1 Branded ID Types (types/ids.ts)

Prevents mixing SessionId and AgentId at compile time:

type SessionId = string & { readonly __brand: 'SessionId' }
type AgentId = string & { readonly __brand: 'AgentId' }

AgentId format: a + optional <label>- + 16 hex chars Pattern: /^a(?:.+-)?[0-9a-f]{16}$/

Export Description
SessionId Branded session ID type
AgentId Branded agent ID type
asSessionId(id) Cast string to SessionId
asAgentId(id) Cast string to AgentId
toAgentId(s) Validate + brand as AgentId, or null

12.2 Command Types (types/command.ts)

PromptCommand — Skill/command that generates a prompt:

type PromptCommand = {
  type: 'prompt'
  progressMessage: string
  contentLength: number
  argNames?: string[]
  allowedTools?: string[]
  model?: string
  source: SettingSource | 'builtin' | 'mcp' | 'plugin' | 'bundled'
  pluginInfo?: { pluginManifest: PluginManifest; repository: string }
  disableNonInteractive?: boolean
  hooks?: HooksSettings
  skillRoot?: string
  context?: 'inline' | 'fork'
  agent?: string
  effort?: EffortValue
  paths?: string[]  // glob patterns — skill only visible after model touches matching files
  getPromptForCommand(args, context): Promise<ContentBlockParam[]>
}

Execution contexts:

  • 'inline' (default): Skill content expands into current conversation
  • 'fork': Skill runs as sub-agent with separate context and token budget

12.3 Plugin Types (types/plugin.ts)

See Section 6.4 for LoadedPlugin, BuiltinPluginDefinition, PluginError, and PluginLoadResult.


13. Remote Session System (remote/)

13.1 System Overview

The remote session system manages multi-user and remote-connected Claude Code sessions, bridging between the local CLI process and remote session infrastructure.

Source: src/remote/

Files:

  • SessionsWebSocket.ts — WebSocket client for server-sent session events
  • RemoteSessionManager.ts — Session state management and reconnection logic
  • remotePermissionBridge.ts — Permission request routing for remote sessions
  • sdkMessageAdapter.ts — Adapts SDK message format to internal message format

13.2 Component Roles

SessionsWebSocket — Maintains a WebSocket connection to the remote session server. Handles:

  • Reconnection with exponential backoff
  • Session event routing
  • Auth token refresh integration

RemoteSessionManager — Top-level orchestrator for remote sessions:

  • Manages session lifecycle (create, attach, detach, resume)
  • Tracks active remote sessions
  • Routes permission requests to appropriate sessions
  • Handles session backgrounding/foregrounding

remotePermissionBridge — Bridges permission requests from remote sessions back to the local UI:

  • Permission requests originating in remote worker sessions are forwarded to the interactive CLI
  • Responses are sent back to the worker via the session infrastructure

sdkMessageAdapter — Converts between SDK wire-format messages and the internal Message type used by the UI layer.


End of spec document.