Skip to content

Episode 12: Startup & Bootstrap — From claude to First Prompt

🌐 Language: English | 中文版 → 📖 Read Online → — Sidebar nav, dark mode & full-text search. Better than raw GitHub.

Source files: cli.tsx (303 lines), init.ts (341 lines), setup.ts (478 lines), main.tsx (4,500+ lines), bootstrap/state.ts (1,759 lines), startupProfiler.ts (195 lines), apiPreconnect.ts (72 lines)

One-liner: Claude Code's startup is a carefully orchestrated race — a cascade of fast-paths, dynamic imports, parallel prefetches, and API preconnection designed to get the user typing as fast as possible while deferring 400KB+ of OpenTelemetry, plugins, and analytics to the background.

Architecture Overview

12 startup bootstrap


Phase 0: The Fast-Path Cascade

cli.tsx (303 lines) is the true entry point. Its design principle: never load more than you need.

Zero-Import Fast-Path

typescript
// 源码位置: src/cli.tsx:25-30
// --version: Zero module loading needed
if (args.length === 1 && (args[0] === '--version' || args[0] === '-v')) {
  console.log(`${MACRO.VERSION} (Claude Code)`)  // Build-time constant
  return
}

MACRO.VERSION is inlined at build time — no imports, no config, no disk I/O.

The Fast-Path Hierarchy

Fast-PathTriggerWhat LoadsWhat Skips
--version-v, --versionNothingEverything
--dump-system-promptAnt-only flagconfig.js, prompts.jsUI, auth, analytics
--daemon-workerSupervisored spawnWorker-specific modulesConfig, analytics, sinks
remote-controlrc, remote, bridgeBridge + auth + policyFull CLI, UI
daemondaemon subcommandConfig + sinks + daemonFull CLI, UI
ps/logs/attach/killBackground sessionsConfig + bg moduleFull CLI, UI
new/list/replyTemplatesTemplate handlerFull CLI
--worktree --tmuxCombined flagsConfig + worktreeMay skip full CLI
(default)Normal startupmain.tsx (everything)Nothing

Every fast-path uses dynamic await import() so the module tree is loaded only when that path is taken.

Early Input Capture

typescript
// Before loading main.tsx (which triggers heavy module eval)
const { startCapturingEarlyInput } = await import('../utils/earlyInput.js')
startCapturingEarlyInput()

This buffers keystrokes during the ~500ms module evaluation window so the user can start typing before the REPL is ready.


Phase 1: Module Evaluation

When main.tsx loads, it triggers a cascade of 200+ static import evaluations. The startup profiler tracks key milestones:

typescript
// main.tsx top-level
import { profileCheckpoint } from './utils/startupProfiler.js'
profileCheckpoint('main_tsx_entry')  // Before heavy imports

// ... 200+ imports ...

profileCheckpoint('main_tsx_imports_loaded')  // After all imports

The Settings Bootstrap

typescript
// main.tsx: eagerLoadSettings()
profileCheckpoint('eagerLoadSettings_start')
// Read settings.json, .claude/settings.json, etc.
// Apply environment variables from settings
profileCheckpoint('eagerLoadSettings_end')

Settings must load eagerly because they influence module-level constants (e.g., DISABLE_BACKGROUND_TASKS captured by BashTool at import time).


Phase 2: Initialization (init.ts)

init() (341 lines, memoized — runs exactly once) handles trust-independent setup:

Execution Order

1. enableConfigs()                    — Validate and enable configuration system
2. applySafeConfigEnvironmentVariables() — Apply safe env vars before trust dialog
3. applyExtraCACertsFromConfig()      — Must happen before first TLS handshake
4. setupGracefulShutdown()            — Register SIGINT/SIGTERM handlers
5. initialize1PEventLogging()         — Lazy: OpenTelemetry sdk-logs (deferred)
6. populateOAuthAccountInfoIfNeeded() — Async: fill OAuth cache
7. initJetBrainsDetection()           — Async: detect IDE
8. detectCurrentRepository()          — Async: populate git cache
9. configureGlobalMTLS()              — mTLS certificate configuration
10. configureGlobalAgents()           — HTTP proxy agents
11. preconnectAnthropicApi()          — Fire-and-forget HEAD request
12. setShellIfWindows()               — Git-bash detection on Windows
13. ensureScratchpadDir()             — Create temp directory if enabled

API Preconnection

typescript
// 源码位置: src/utils/apiPreconnect.ts:10-25
export function preconnectAnthropicApi(): void {
  // Skip if using proxy/mTLS/unix socket (SDK uses different transport)
  // Skip if using Bedrock/Vertex/Foundry (different endpoints)

  const baseUrl = process.env.ANTHROPIC_BASE_URL || getOauthConfig().BASE_API_URL
  // Fire and forget — 10s timeout, errors silently caught
  void fetch(baseUrl, {
    method: 'HEAD',
    signal: AbortSignal.timeout(10_000),
  }).catch(() => {})
}

The TCP+TLS handshake costs ~100-200ms. By firing it during init, the warmed connection is ready by the time the first API call happens. Bun's fetch shares a keep-alive connection pool globally.

Lazy Telemetry Loading

typescript
// OpenTelemetry is ~400KB + protobuf modules
// gRPC exporters add another ~700KB via @grpc/grpc-js
// All deferred until telemetry is actually initialized
const { initializeTelemetry } = await import('../utils/telemetry/instrumentation.js')

Phase 3: Setup (setup.ts)

setup() (478 lines) runs after trust is established. It handles environment preparation:

Key Operations

  1. UDS Messaging Server — Unix domain socket for inter-process messaging (deferred if --bare)
  2. Teammate Snapshot — Capture swarm teammate state (deferred if --bare)
  3. Terminal Backup Restoration — Detect interrupted iTerm2/Terminal.app setups
  4. CWD + HookssetCwd() must run before anything that depends on path, then captureHooksConfigSnapshot()
  5. Worktree Creation — If --worktree, create git worktree and chdir into it
  6. Background Jobs — Fire-and-forget prefetches

Background Prefetch Strategy

typescript
// Background jobs - only critical registrations before first query
initSessionMemory()     // Synchronous - registers hook, gate check lazy
void getCommands()      // Prefetch commands (parallel with user input)
void loadPluginHooks()  // Pre-load plugin hooks for SessionStart

// Deferred to next tick so git subprocess doesn't block first render
setImmediate(() => {
  void registerAttributionHooks()
})

void prefetchApiKeyFromApiKeyHelperIfSafe()

// Await only if release notes exist (interactive only)
if (!isBareMode()) {
  const { hasReleaseNotes } = await checkForReleaseNotes(...)
}

The --bare Mode

The --bare flag (called "SIMPLE" internally) short-circuits massive amounts of startup:

Skipped in --bareReason
UDS messaging serverScripted calls don't receive injected messages
Teammate snapshotNo swarm in bare mode
Terminal backup checksNon-interactive
Plugin prefetchexecuteHooks early-returns under --bare
Attribution hooksScripted calls don't commit code
Session file access hooksNo usage metrics needed
Team memory watcherNo team memory in scripted mode
Release notesNon-interactive
Recent activityNon-interactive

The Bootstrap State Singleton

bootstrap/state.ts (1,759 lines) is the global state store — the only place session-wide mutable state lives.

Design Constraints

typescript
// DO NOT ADD MORE STATE HERE - BE JUDICIOUS WITH GLOBAL STATE
// ALSO HERE - THINK THRICE BEFORE MODIFYING
// AND ESPECIALLY HERE

The file contains stern warnings because it's the DAG leaf — every module can import it, but it imports almost nothing.

Key State Categories (80+ fields)

CategoryExamplesLifetime
IdentitysessionId, originalCwd, projectRoot, cwdSession
Cost TrackingtotalCostUSD, totalAPIDuration, modelUsageSession
Turn MetricsturnToolDurationMs, turnHookCount, turnClassifierCountPer-turn (reset each query)
Telemetrymeter, sessionCounter, loggerProvider, tracerProviderLazy init
API StatelastAPIRequest, lastMainRequestId, lastApiCompletionTimestampRolling
Cache LatchesafkModeHeaderLatched, fastModeHeaderLatched, cacheEditingHeaderLatchedSticky-on (never unset)
Feature StateinvokedSkills, planSlugCache, systemPromptSectionCacheSession

Sticky Latches for Cache Preservation

typescript
// Once auto mode is activated, keep sending the header forever
// So Shift+Tab toggles don't bust the prompt cache
afkModeHeaderLatched: boolean | null  // null = not yet, true = latched

// Once fast mode is enabled, keep sending the header
// So cooldown enter/exit doesn't double-bust the cache
fastModeHeaderLatched: boolean | null

// Once cache editing is enabled, keep the header
// So mid-session GrowthBook toggles don't bust the cache
cacheEditingHeaderLatched: boolean | null

These are "sticky-on" — once set to true, they never go back to false. This pattern prevents prompt cache busting from feature toggles during a session.

Scroll Drain Suspension

typescript
let scrollDraining = false
const SCROLL_DRAIN_IDLE_MS = 150

export function markScrollActivity(): void {
  scrollDraining = true
  // Background intervals check getIsScrollDraining() and skip work
  // so they don't compete with scroll frames for the event loop
}

During active scrolling, background intervals (analytics, file watchers) voluntarily yield to keep the UI responsive.


The Startup Profiler

startupProfiler.ts (195 lines) provides performance instrumentation for the entire startup path.

Two Modes

ModeTriggerSamplingOutput
SampledAlways (100% ant, 0.5% external)Per-session randomStatsig tengu_startup_perf event
DetailedCLAUDE_CODE_PROFILE_STARTUP=1100%Full report with memory snapshots to ~/.claude/startup-perf/

Phase Definitions

typescript
// 源码位置: src/utils/startupProfiler.ts:30-40
const PHASE_DEFINITIONS = {
  import_time: ['cli_entry', 'main_tsx_imports_loaded'],
  init_time: ['init_function_start', 'init_function_end'],
  settings_time: ['eagerLoadSettings_start', 'eagerLoadSettings_end'],
  total_time: ['cli_entry', 'main_after_run'],
}

Checkpoint Timeline (Normal Startup)

cli_entry                          →  t=0ms
cli_before_main_import             →  ~5ms (early input buffer set up)
main_tsx_entry                     →  ~10ms
main_tsx_imports_loaded            →  ~200-500ms (200+ modules evaluated)
eagerLoadSettings_start            →  ~500ms
eagerLoadSettings_end              →  ~520ms
main_function_start                →  ~520ms
init_function_start                →  ~525ms
init_configs_enabled               →  ~530ms
init_network_configured            →  ~540ms (mTLS + proxy)
init_function_end                  →  ~550ms
setup_before_prefetch              →  ~600ms
setup_after_prefetch               →  ~610ms
action_handler_start               →  ~650ms
action_tools_loaded                →  ~700ms
action_after_setup                 →  ~750ms
action_commands_loaded             →  ~800ms
action_mcp_configs_loaded          →  ~850ms
action_after_plugins_init          →  ~900ms (if plugins exist)
run_before_parse                   →  ~950ms
run_after_parse                    →  ~960ms
main_after_run                     →  ~1000ms (REPL ready)

Transferable Design Patterns

The following patterns can be directly applied to other CLI tools or process bootstrapping systems.

Pattern 1: Fast-Path Cascade with Dynamic Imports

Scenario: A CLI tool has a ~1s startup budget but a 200+ file module tree. Practice: Use dynamic await import() to create a cascade of fast-paths, each loading only what it needs. Claude Code application: --version loads 0 imports (~5ms), --daemon-worker loads ~10 (~50ms), normal loads 200+ (~500-1000ms).

Pattern 2: DAG Leaf for Global State

Scenario: A global state singleton risks creating circular dependencies if it imports from the module tree. Practice: Make the state module the leaf of the dependency graph — it imports almost nothing, enforced by a lint rule. Claude Code application: bootstrap/state.ts imports only crypto/lodash/process; an ESLint bootstrap-isolation rule prevents deeper imports.

Pattern 3: Preconnection vs Prefetch

Scenario: Startup needs to warm multiple resources (network, data) without blocking. Practice: Separate TCP+TLS preconnection (fire-and-forget HEAD) from data prefetch (fire-and-forget async calls), both non-blocking. Claude Code application: preconnectAnthropicApi() saves ~100-200ms on first API call; void getCommands() prefetches data in parallel.

The Ablation Baseline

typescript
// cli.tsx: Science L0 ablation baseline
if (feature('ABLATION_BASELINE') && process.env.CLAUDE_CODE_ABLATION_BASELINE) {
  // Disable: thinking, compact, auto-compact, auto-memory, background tasks
  // This creates a "baseline" for measuring the impact of each feature
}

This environment variable disables all advanced features at once, creating a controlled baseline for A/B testing individual features' impact on user experience.


Component Summary

ComponentLinesRole
main.tsx4,500+Full CLI: commander setup, action handlers, REPL orchestration
bootstrap/state.ts1,759Global state singleton: 80+ fields, DAG leaf, sticky latches
setup.ts478Post-trust environment: worktrees, hooks, background prefetches
init.ts341Trust-independent init: configs, TLS, proxy, preconnect
cli.tsx303Entry point: fast-path cascade, dynamic import dispatch
startupProfiler.ts195Startup performance: checkpoints, phases, memory snapshots
apiPreconnect.ts72TCP+TLS warm-up: fire-and-forget HEAD to Anthropic API

Next: Episode 13 — Bridge System →

← Episode 11 — Compact System | Episode 13 →

Released under the MIT License.