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

1767 lines
63 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Claude Code — Special Systems: Buddy, Memory, Keybindings, Skills, Voice, Plugins & More
---
## Table of Contents
1. [Buddy (Companion/Tamagotchi) System](#1-buddy-companiontagotchi-system)
2. [Memory Directory (memdir) System](#2-memory-directory-memdir-system)
3. [Keybindings System](#3-keybindings-system)
4. [Skills System](#4-skills-system)
5. [Voice System](#5-voice-system)
6. [Plugins System](#6-plugins-system)
7. [Output Styles](#7-output-styles)
8. [Hooks Schema](#8-hooks-schema)
9. [Native-TypeScript Ports (native-ts/)](#9-native-typescript-ports-native-ts)
10. [MoreRight Hook (moreright/)](#10-moreright-hook-moreright)
11. [Migrations](#11-migrations)
12. [Core Type Definitions (types/)](#12-core-type-definitions-types)
13. [Remote Session System (remote/)](#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.
```typescript
// 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:**
```typescript
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`
```typescript
export const RARITIES = ['common', 'uncommon', 'rare', 'epic', 'legendary'] as const
export type Rarity = (typeof RARITIES)[number]
```
#### `RARITY_WEIGHTS`
```typescript
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)
```typescript
const RARITY_FLOOR: Record<Rarity, number> = {
common: 5,
uncommon: 15,
rare: 25,
epic: 35,
legendary: 50,
}
```
#### `RARITY_STARS` (UI display)
```typescript
export const RARITY_STARS = {
common: '★',
uncommon: '★★',
rare: '★★★',
epic: '★★★★',
legendary: '★★★★★',
}
```
#### `RARITY_COLORS` (maps to Theme keys)
```typescript
export const RARITY_COLORS = {
common: 'inactive',
uncommon: 'success',
rare: 'permission',
epic: 'autoAccept',
legendary: 'warning',
}
```
#### `Species` (18 total)
```typescript
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)
```typescript
export const EYES = ['·', '✦', '×', '◉', '@', '°'] as const
```
#### `Hat` (8 types)
```typescript
export const HATS = [
'none', 'crown', 'tophat', 'propeller',
'halo', 'wizard', 'beanie', 'tinyduck',
] as const
```
#### `StatName` (5 stats)
```typescript
export const STAT_NAMES = ['DEBUGGING', 'PATIENCE', 'CHAOS', 'WISDOM', 'SNARK'] as const
```
#### `CompanionBones` (deterministic, never stored)
```typescript
export type CompanionBones = {
rarity: Rarity
species: Species
eye: Eye
hat: Hat
shiny: boolean
stats: Record<StatName, number> // 1100 per stat
}
```
#### `CompanionSoul` (AI-generated, stored)
```typescript
export type CompanionSoul = {
name: string
personality: string
}
```
#### `StoredCompanion` (what persists in config)
```typescript
export type StoredCompanion = CompanionSoul & { hatchedAt: number }
```
#### `Companion` (runtime assembled)
```typescript
export type Companion = CompanionBones & CompanionSoul & { hatchedAt: number }
```
#### `Roll` (output of `rollFrom`)
```typescript
export type Roll = {
bones: CompanionBones
inspirationSeed: number // Passed to AI for deterministic soul generation
}
```
---
### 1.6 Stat Rolling Algorithm
```typescript
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:**
```typescript
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):
```typescript
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
```markdown
---
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:**
```typescript
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:**
```typescript
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:**
```typescript
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: `esc``escape`, `return``enter`, `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`:**
```typescript
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)
```typescript
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:
```json
{
"$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`:**
```typescript
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:
```typescript
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:**
```typescript
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:** `space` `voice: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`):
```typescript
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:
```typescript
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`)
```typescript
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`:**
```typescript
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`):
```typescript
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'`)
```typescript
{
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'`)
```typescript
{
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'`)
```typescript
{
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'`)
```typescript
{
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
```typescript
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:**
```typescript
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:**
```typescript
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:**
```typescript
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
```typescript
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-latest``opus`
- `fennec-fast-latest` / `opus-4-5-fast``opus[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:
```typescript
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:
```typescript
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.*