markdown.engineering
Lesson 01

The Claude Code Boot Sequence

From claude keystroke to interactive REPL — a deep dive into every phase of startup.

01 Overview

When you type claude in your terminal, a sophisticated multi-phase boot pipeline runs before you see the first prompt. Understanding this pipeline helps you: reason about startup latency, debug weird first-launch behaviors, and appreciate the careful parallelism engineers built in to keep time-to-interactive low.

Source files covered
entrypoints/cli.tsxmain.tsxsetup.tsbootstrap/state.tsreplLauncher.tsxink.ts

At the highest level, boot happens in three nested layers:

Layer 1

CLI Entrypoint

cli.tsx — zero-cost fast paths, environment prep, argv dispatch

Layer 2

Main Function

main.tsx — Commander parsing, init, migrations, permission checks

Layer 3

Setup + REPL

setup.ts + replLauncher.tsx — session wiring, Ink render

02 Boot Pipeline Flowchart

The diagram below traces the exact call sequence from process entry to first render:

flowchart TD A["Process starts\ncli.tsx — main()"] --> B["Set COREPACK_ENABLE_AUTO_PIN=0\nSet NODE_OPTIONS if CCR remote"] B --> C{"Fast-path\ncheck"} C -->|"--version / -v"| V["Print version\nprocess.exit(0)"] C -->|"--daemon-worker"| D["runDaemonWorker()"] C -->|"remote-control / bridge"| BR["bridgeMain()"] C -->|"no fast-path"| E["profileCheckpoint('cli_entry')\nDynamic import main.tsx"] E --> F["main() in main.tsx\nprofileCheckpoint('main_function_start')"] F --> G["startMdmRawRead()\nstartKeychainPrefetch()\n[top-level side effects]"] G --> H["profileCheckpoint('main_tsx_entry')\nLoad all static imports\n~135ms module eval"] H --> I["profileCheckpoint('main_tsx_imports_loaded')"] I --> J["initializeWarningHandler()\nRegister SIGINT / exit handlers"] J --> K["eagerLoadSettings()\n--settings / --setting-sources early parse"] K --> L["Commander.parse()\nResolve: cwd, permissionMode,\nmodel, session flags..."] L --> M["init() from entrypoints/init.ts\napplySafeConfigEnvironmentVariables()\nensureMdmSettingsLoaded()"] M --> N["runMigrations()\nSettings schema upgrades\nmigrationVersion check"] N --> O["setup(cwd, permissionMode, ...)"] O --> P["Node.js ≥18 check\nCustom session ID if --session"] P --> Q{"isBareMode?"} Q -->|"No"| R["startUdsMessaging()\ncaptureTeammateModeSnapshot()"] Q -->|"Yes"| S["Skip UDS + teammate snapshot"] R --> T["setCwd(cwd)\ncaptureHooksConfigSnapshot()\ninitializeFileChangedWatcher()"] S --> T T --> U{"--worktree\nflag?"} U -->|"Yes"| WT["createWorktreeForSession()\ncreateT­muxSessionForWorktree()\nsetProjectRoot()"] U -->|"No"| BG["Background jobs:\ninitSessionMemory()\ngetCommands() prefetch\nloadPluginHooks()"] WT --> BG BG --> AN["initSinks()\nlogEvent('tengu_started')"] AN --> PF["prefetchApiKeyFromApiKeyHelperIfSafe()\ncheckForReleaseNotes()"] PF --> PERM["Permission safety checks\n(root/sudo guard,\nsandbox gate for ants)"] PERM --> PREV["Log previous session exit metrics\nfrom projectConfig"] PREV --> REPL["launchRepl()\nreplLauncher.tsx"] REPL --> INK["import App + REPL components\nrenderAndRun() via ink.ts"] INK --> DONE["First render — user sees prompt"] DONE --> DEF["startDeferredPrefetches()\ninitUser / getUserContext /\nsettingsChangeDetector ..."] style A fill:#22201d,stroke:#7d9ab8,color:#b8b0a4 style DONE fill:#1d211b,stroke:#6e9468,color:#b8b0a4 style V fill:#241816,stroke:#c47a50,color:#b8b0a4 style REPL fill:#1f1b24,stroke:#8e82ad,color:#b8b0a4
03 Phase-by-Phase Walkthrough

Phase 1 — CLI Entrypoint (cli.tsx)

cli.tsx is a deliberate thin bootstrap. All imports are dynamic so the "zero module" fast paths (--version, --daemon-worker, --claude-in-chrome-mcp) return without loading any of the heavy CLI surface. This is a significant UX win — claude --version returns in milliseconds.

// cli.tsx — fast-path: --version needs zero imports
const args = process.argv.slice(2)
if (args.length === 1 && (args[0] === '--version' || args[0] === '-v')) {
  console.log(`${MACRO.VERSION} (Claude Code)`)
  return
}

// For all other paths, load the startup profiler first
const { profileCheckpoint } = await import('../utils/startupProfiler.js')
profileCheckpoint('cli_entry')
Design pattern
The feature('X') calls are build-time flags (Bun dead-code elimination). Features like BRIDGE_MODE, DAEMON, SSH_REMOTE can be stripped from external builds entirely. The CLI dispatch table is open-closed: new fast-paths are added here without touching main.tsx.

Before loading main.tsx, cli.tsx also handles environment mutations that must happen at process start, before any module evaluates: COREPACK pinning is disabled, and CCR containers get an 8 GB heap cap via NODE_OPTIONS.

Phase 2 — Parallel prefetch side-effects (main.tsx top-level)

The very top of main.tsx has three side-effects that execute before any other imports are evaluated — marked with an ESLint disable comment to make the intent explicit:

// These side-effects must run before all other imports:
profileCheckpoint('main_tsx_entry')   // timestamp: module eval started

startMdmRawRead()     // fires plutil/reg query subprocesses in parallel
startKeychainPrefetch() // starts macOS keychain reads (OAuth + API key)

By the time the ~135ms of static imports finish loading, MDM policy and keychain reads are already in flight. This parallelism shaves significant time from first-auth flows.

Deep dive — Why fire MDM reads this early?

MDM (Mobile Device Management) on macOS stores enterprise policy in the defaults domain. Reading it requires spawning plutil or reg query on Windows. These subprocesses take ~20–40ms each.

applySafeConfigEnvironmentVariables() (called inside init()) needs MDM policy to be loaded before it can apply any managed settings. By firing startMdmRawRead() at module eval time, the subprocess runs concurrently with the remaining import chain, so by the time init() calls ensureMdmSettingsLoaded(), the result is already in cache.

Similarly, startKeychainPrefetch() fires two async macOS keychain reads for OAuth token and legacy API key. Without this, the reads would happen sequentially inside applySafeConfigEnvironmentVariables() via sync spawn — measured at ~65ms on every macOS startup.

Phase 3 — Commander argument parsing

After imports load, main() calls eagerLoadSettings() to handle --settings and --setting-sources flags before Commander even runs, then invokes Commander's .parse(). Commander resolves and validates: cwd, permissionMode, --print/-p mode, --model, --resume, --session, MCP server configs, and many more flags.

// main.tsx — runs migrations once per config version bump
const CURRENT_MIGRATION_VERSION = 11
function runMigrations(): void {
  if (getGlobalConfig().migrationVersion !== CURRENT_MIGRATION_VERSION) {
    migrateAutoUpdatesToSettings()
    migrateSonnet45ToSonnet46()  // example: model string upgrades
    migrateOpusToOpus1m()
    // ...8 more migration functions...
    saveGlobalConfig(prev => ({ ...prev, migrationVersion: CURRENT_MIGRATION_VERSION }))
  }
}
Gotcha
Migrations run on every process start but are gated by migrationVersion. If you downgrade Claude Code, the migration version is already advanced and migrations won't re-run — which can cause subtle config inconsistencies.

Phase 4 — setup() in setup.ts

setup() is where the session is actually wired up. It receives the parsed arguments and performs checks in a carefully ordered sequence:

  1. Node.js version gate (≥18 required)
  2. Optional custom session ID via switchSession()
  3. UDS (Unix Domain Socket) messaging server startup — so hook processes can find the socket
  4. Teammate/swarm snapshot (non-bare mode only)
  5. iTerm2 and Terminal.app backup restoration for interrupted setups
  6. setCwd(cwd) — must happen before anything that reads cwd
  7. Hooks config snapshot — reads .claude/settings.json from the new cwd
  8. FileChanged hook watcher initialization
  9. Optional worktree creation + tmux session
  10. Background jobs: initSessionMemory(), getCommands(), plugin hooks
  11. initSinks() — attaches analytics + error sinks, drains queued events
  12. logEvent('tengu_started') — the first reliable "process started" beacon
  13. API key prefetch (safe path only)
  14. Release notes check + recent activity fetch
  15. Permission safety checks (root/sudo guard, Docker sandbox gate)
  16. Previous session exit metrics logged from projectConfig
// setup.ts — setCwd ordering comment (verbatim from source)
// IMPORTANT: setCwd() must be called before any other code that depends on the cwd
setCwd(cwd)

// IMPORTANT: Must be called AFTER setCwd() so hooks are loaded from the correct directory
captureHooksConfigSnapshot()
Deep dive — The tengu_started beacon

The comment in the source explains why this event is placed precisely where it is:

"Session-success-rate denominator. Emit immediately after the analytics sink is attached — before any parsing, fetching, or I/O that could throw. … This beacon is the earliest reliable 'process started' signal for release health monitoring."

The comment references a specific incident (inc-3694) where a crash in checkForReleaseNotes() meant every event after it was dead. The beacon placement ensures the denominator is recorded even when downstream code throws.

Deep dive — Bare mode (--bare / CLAUDE_CODE_SIMPLE)

Several steps are guarded by !isBareMode(). Bare mode is used for scripted/SDK calls (claude -p "..." style). In bare mode, the following are skipped:

  • UDS messaging server (no hook injection)
  • Teammate snapshot (swarm not used)
  • Session memory initialization
  • Plugin hook pre-loading
  • Attribution hooks + repo classification
  • All deferred prefetches (startDeferredPrefetches())

The design principle: bare mode is latency-sensitive. Every millisecond saved here matters when you're calling Claude from a CI pipeline hundreds of times a day.

Deep dive — Worktree + tmux creation

When --worktree is passed, setup() creates a git worktree for the session before anything else touches the filesystem. The order matters:

  1. Resolve canonical git root (handles being invoked from inside an existing worktree)
  2. Generate a slug from getPlanSlug() or PR number
  3. Call createWorktreeForSession() — delegates to WorktreeCreate hook if configured
  4. Optionally create a tmux session pointing at the worktree path
  5. Call setCwd(worktreePath) and setProjectRoot()
  6. Call clearMemoryFileCaches() since cwd changed
  7. Re-capture hooks config from the worktree's .claude/settings.json

The setProjectRoot() call here is important: it fixes the project identity (session history, skills, CLAUDE.md) to the worktree root for the duration of the session, rather than the original repo root.

Phase 5 — Global State (bootstrap/state.ts)

state.ts is the single source of truth for all session-scoped global state. The comment at the top says it plainly: "DO NOT ADD MORE STATE HERE — BE JUDICIOUS WITH GLOBAL STATE."

State tracked includes:

Identity

Session & Paths

sessionId, originalCwd, projectRoot, cwd

Costs

Usage Tracking

totalCostUSD, modelUsage, token counters, FPS metrics

Flags

Session Mode

isInteractive, sessionBypassPermissionsMode, isRemoteMode

Telemetry

OTel Providers

meter, loggerProvider, tracerProvider

Cache

Prompt Cache

promptCache1hEligible, afkModeHeaderLatched, fastModeHeaderLatched

Hooks

Runtime Hooks

registeredHooks, invokedSkills, sessionCronTasks

// bootstrap/state.ts — initial state factory (simplified excerpt)
function getInitialState(): State {
  let resolvedCwd = ''
  try {
    // Resolve symlinks so session storage paths are consistent
    resolvedCwd = realpathSync(cwd())
  } catch { resolvedCwd = cwd() }

  return {
    originalCwd: resolvedCwd,
    projectRoot: resolvedCwd,
    sessionId: asSessionId(randomUUID()),
    isInteractive: true,
    totalCostUSD: 0,
    // ... ~60 more fields
  }
}
Prompt cache stability
Several latching fields (afkModeHeaderLatched, fastModeHeaderLatched, thinkingClearLatched) exist specifically to keep Anthropic API prompt cache headers stable throughout a session. Once a header mode is activated, it stays activated even if the user toggles settings mid-session — changing the header would bust the expensive server-side cache.

Phase 6 — Ink render (replLauncher.tsx + ink.ts)

The final step is rendering the React-based TUI. launchRepl() dynamically imports App and REPL components (avoiding circular imports) then calls renderAndRun():

// replLauncher.tsx
export async function launchRepl(
  root: Root,
  appProps: AppWrapperProps,
  replProps: REPLProps,
  renderAndRun: (root: Root, element: React.ReactNode) => Promise<void>,
): Promise<void> {
  const { App }  = await import('./components/App.js')
  const { REPL } = await import('./screens/REPL.js')
  await renderAndRun(root, <App {...appProps}><REPL {...replProps} /></App>)
}

ink.ts wraps every render call with <ThemeProvider> automatically, so ThemedBox and ThemedText components work without each call site having to mount the theme context:

// ink.ts — wraps every render with ThemeProvider
function withTheme(node: ReactNode): ReactNode {
  return createElement(ThemeProvider, null, node)
}

export async function render(node, options) {
  return inkRender(withTheme(node), options)
}
After first render
Once the REPL is on screen, startDeferredPrefetches() fires background work that's NOT needed for the first render: initUser(), getUserContext(), MCP URL prefetch, model capability refresh, and file change detector initialization. This work runs while the user is typing their first message — hidden by the human reaction time window.
04 Key Takeaways

What to remember from this lesson

  • The boot sequence is three nested layers: CLI entrypoint → main function → setup + REPL render.
  • Fast paths in cli.tsx exit before loading any heavy modules. claude --version never touches main.tsx.
  • MDM and keychain reads are fired at module-eval time to parallelize with the ~135ms import chain — a key startup latency optimization.
  • setCwd() must come before captureHooksConfigSnapshot(). The ordering is enforced by comments in the code and violating it produces wrong hook configs.
  • Bare mode (--bare) strips every non-essential startup step for scripted/SDK use-cases. Understanding what's skipped explains why bare mode is faster.
  • bootstrap/state.ts is the global state ledger. Prompt cache latch fields ensure API headers stay stable across mid-session toggles to protect the server-side cache.
  • The tengu_started event is the earliest reliable beacon; everything after initSinks() counts toward session success rate.
  • Deferred prefetches run after first render, hidden in the human typing window — architecture designed around perceived latency, not just raw latency.
05 Knowledge Check

Quiz — 5 Questions

Q1. What is the primary purpose of the fast-path checks at the top of cli.tsx?
Correct! Fast paths in cli.tsx use dynamic imports so subcommands like --version, --daemon-worker, and remote-control exit without ever importing the heavy main.tsx module graph.
Q2. Why are startMdmRawRead() and startKeychainPrefetch() called as top-level side effects at the very start of main.tsx, before other imports?
Correct! MDM policy reads (via plutil) and keychain reads take 20–65ms. By launching them during module evaluation they run concurrently with the import chain and are already resolved when init() needs them. Sequential reads would add that latency to the critical path.
Q3. In setup.ts, why must setCwd(cwd) be called before captureHooksConfigSnapshot()?
Correct! The source comment is explicit: "Must be called AFTER setCwd() so hooks are loaded from the correct directory." captureHooksConfigSnapshot() reads the project's settings file to snapshot which hooks are configured — if cwd is still the shell's working directory rather than the intended project root, the wrong hooks get loaded.
Q4. What does "bare mode" (--bare / CLAUDE_CODE_SIMPLE) skip during boot?
Correct! Bare mode skips every piece of non-essential startup work that doesn't apply to scripted/SDK calls: no UDS socket, no teammate snapshot, no session memory, no plugin prefetch, no attribution hooks, no repo classification, and no deferred prefetches. It still runs auth checks, migrations, and the analytics beacon.
Q5. Why do fields like afkModeHeaderLatched and fastModeHeaderLatched exist in bootstrap/state.ts?
Correct! The comments in state.ts explain these are "sticky-on latches." Once AFK mode, fast mode, or cache-editing mode is first activated, the corresponding API header stays on for the rest of the session. If the header toggled with each GrowthBook/settings change, the server's cached prompt prefix would be busted, causing expensive cache misses on the Anthropic side (~50–70K tokens re-processed).
0/5