# The Boot Sequence: From Typing `claude` to System Ready

From the moment the user types `claude` to when the system is ready to accept input, three phases unfold—Pre-import preloading, initialization, and UI rendering. This chapter dissects the implementation details of the boot sequence, along with the trade-offs in optimizations like preconnection and deferred loading.

> **🌍 Industry Context: The CLI Boot Sequence Is Standard Engineering Practice**
>
> Nearly every CLI tool must solve the "what to do at startup" problem; the difference lies in complexity and optimization:
>
> - **Aider** (Python CLI AI coding assistant): Relatively simple startup—parse arguments, load config, establish API connection. Python module loading is itself slow, but Aider's dependency tree is far smaller than Claude Code's, so startup overhead is mainly the Python interpreter.
> - **Cursor** (VS Code extension): As an editor extension, its "startup" is extension activation (`activate()`), not launching a standalone process. The VS Code extension host manages the lifecycle, a completely different problem domain from CLI cold start.
> - **gh CLI** (GitHub's official CLI): A single statically-linked Go binary; startup is virtually instantaneous—no module loading problem, Go's static linking naturally eliminates this overhead.
> - **Vercel CLI**: Also a Node.js CLI, facing the same slow module loading; mitigated via `pkg` bundling and lazy-loading subcommands.
>
> What makes Claude Code special is that it is a **heavyweight Node.js/Bun CLI** (a dependency tree of 1,884 files) that must simultaneously perform I/O-heavy operations like credential reading and API preconnection during startup. Hence it runs I/O in parallel during the Pre-import phase—this is not a novel technique, but it is indeed uncommon among JS/TS CLI tools, because most do not have such a heavy module-loading burden.

---

## Prologue: The Three Stages of Booting a Computer

After you press the power button, three things happen:
1. **BIOS/UEFI** (firmware) wakes up—performs basic hardware checks and locates the disk containing the OS
2. **Bootloader** loads—reads the OS kernel from disk into memory
3. **Kernel Init**—starts all subsystems, finally displaying the login screen

Claude Code's startup also has three stages:
1. **Pre-import stage**—launches critical I/O operations *before* the bulk of module loading
2. **Initialization stage**—loads config, connects to APIs, starts subsystems
3. **Ready stage**—renders the UI and waits for user input; background prefetching continues

> 📚 **Course Connection:** The three boot stages = the OS BIOS → Bootloader → Kernel startup sequence. Pre-import maps to the BIOS firmware layer (doing basic hardware probing before the main system loads), `init()` maps to the Bootloader (loading all subsystems into place), and `launchRepl()` maps to the Kernel finishing initialization and showing the login screen.
>
> **🔑 OS Analogy:** Just like booting a PC: press power to detect hardware (`main.tsx`) → load the OS (`init.ts`) → show the desktop and wait for you to act (`launchRepl()`).
>
> 💡 **Plain English:** The boot sequence is like your **morning routine**—the alarm ringing = CLI startup → washing up = loading config → getting dressed = initializing modules → leaving the house = ready and waiting for input. While you brush your teeth, you heat breakfast (I/O parallel prefetching); before leaving, you check for keys, phone, and wallet (checking credentials and config); once you arrive at work, you can start immediately.

---

## 1. Pre-import Stage: Getting a Head Start in the Gaps of Module Loading

At the **very top** of `main.tsx`, three function calls are **interleaved between import statements**:

```typescript
import { profileCheckpoint } from './utils/startupProfiler.js';
profileCheckpoint('main_tsx_entry');     // Record timestamp

import { startMdmRawRead } from './utils/settings/mdm/rawRead.js';
startMdmRawRead();                       // Start MDM config read

import { startKeychainPrefetch } from './utils/secureStorage/keychainPrefetch.js';
startKeychainPrefetch();                 // Start keychain prefetch

// —— Below come the remaining dozens of imports ——
import { Command as CommanderCommand } from '@commander-js/extra-typings';
import chalk from 'chalk';
// ... (dependency tree of 1,884 files)
```

**Key detail:** This exploits the **Bun bundler's sequential evaluation semantics**—each `import` must finish loading and evaluating that module before the next statement executes. So the actual execution order is: import `profileCheckpoint` → call it → import `startMdmRawRead` → call it → import `startKeychainPrefetch` → call it. Each function depends only on its own import, without waiting for the remaining modules to load. This is more precise than "before all imports"—it is "before the *remaining* bulk of imports."

**Why interleave them this way?**

JavaScript/TypeScript `import` is synchronous—the parser must load and execute all imported modules before continuing. The imports for these three functions are lightweight (a few milliseconds each), but the remaining import chain involves 1,884 files, taking roughly **~135 ms** to load.

These three lines trigger two I/O operations *before* the bulk of module loading:
- `startMdmRawRead()` (`utils/settings/mdm/rawRead.js`): On macOS, spawns a `plutil` subprocess to read the MDM config file; on Windows, uses `reg query` to read the registry. This is a subprocess operation with non-trivial overhead.
- `startKeychainPrefetch()` (`utils/secureStorage/keychainPrefetch.js`): Launches **two** keychain reads in parallel (OAuth token + legacy API key). Without this prefetch, subsequent `applySafeConfigEnvironmentVariables()` would block synchronously for ~65 ms.

Both operations are asynchronous—they run in the background, **in parallel** with the remaining module loading. By the time the ~135 ms of module loading finishes, these two I/O operations may already be done. Later, in the `preAction` phase (`main.tsx:914`), `ensureMdmSettingsLoaded()` and `ensureKeychainPrefetchCompleted()` await their results—most of the time with zero wait, because the results are already ready.

**Analogy:** Checking your phone while waiting for the elevator—not "finish checking your phone, then wait for the elevator," but "check your phone *while* waiting for the elevator." Stuff useful work (I/O prefetching) into unavoidable wait time (module loading).

---

## 2. The `main()` Function: Mode Router

Once module loading completes, the `main()` function executes. Its core responsibility is **deciding which runtime mode to enter**:

```
main()
  ├── Inspect CLI arguments
  │   ├── -p / --print → runHeadless() (single-shot print mode)
  │   ├── mcp serve → Start MCP server mode
  │   └── No special flags → Continue
  │
  ├── Inspect environment variables
  │   ├── CLAUDE_CODE_COORDINATOR_MODE=1 → Coordinator mode
  │   └── KAIROS → Assistant mode (ant-only)
  │
  ├── Inspect feature flags
  │   ├── SSH_REMOTE → SSH remote mode
  │   ├── DIRECT_CONNECT → cc:// URL direct-connect mode
  │   └── BRIDGE_MODE → Bridge mode
  │
  ├── preAction hook (Commander's pre-command hook)
  │   ├── await MDM config + keychain prefetch completion
  │   ├── await init()              ← Subsystem initialization
  │   ├── runMigrations()           ← Migrations run after init()
  │   ├── loadRemoteManagedSettings()  ← Remote settings (non-blocking)
  │   └── loadPolicyLimits()        ← Policy limits (non-blocking)
  │
  └── Default → launchRepl() (interactive REPL)
```

> **Note:** The `preAction` hook is a Commander.js mechanism—it fires **only when executing a command**, so `claude --help` does not call `init()`. This avoids initialization overhead when merely viewing help text.

### Runtime Modes

Core modes (available in all builds):

| Mode | Trigger | Purpose |
|------|---------|---------|
| **REPL** | Default | Interactive terminal conversation |
| **Headless** | `-p/--print` | Single invocation, output and exit |
| **MCP Serve** | `mcp serve` subcommand | Run as an MCP server |

Feature-gated extended modes (controlled by `bun:bundle`'s `feature()` compile-time switch; some internal-only builds):

| Mode | Trigger | Purpose | Note |
|------|---------|---------|------|
| **Coordinator** | Env var `CLAUDE_CODE_COORDINATOR_MODE=1` | Multi-worker orchestration | Feature gate: `COORDINATOR_MODE` |
| **KAIROS** | Feature gate + `assistant` subcommand | Internal Assistant mode | ant-only experimental feature |
| **SSH Remote** | `claude ssh <host>` | Remote terminal | Feature gate: `SSH_REMOTE` |
| **Direct Connect** | `cc://` URL | Connect to a remote Claude Code server | Feature gate: `DIRECT_CONNECT` |
| **Bridge / Remote Control** | `--remote-control` / `--rc` | claude.ai/code remotely controls local environment | Feature gate: `BRIDGE_MODE` |

> **Note:** SDK mode does not go through the `main()` router; it calls Claude Code's API directly via code import, so it is not listed here. Bridge (Remote Control) and Direct Connect are opposite connection directions: Bridge is remote controlling local, while Direct Connect is local connecting to a remote server.

---

## 3. Migration System: Keeping Config and Code in Sync

Inside the `preAction` hook, after `init()` completes, the system calls `runMigrations()` (main.tsx:950) to check `globalConfig.migrationVersion`. If it does not equal the current version (version 11), all migration functions are run in sequence:

```
Version 1 → 2: Model string format migration
Version 2 → 3: Config path migration
...
Version 10 → 11: Latest migration
```

Each migration function is idempotent—running it many times has the same effect as running it once. This guarantees that even if a crash occurs mid-migration, rerunning will not cause errors.

> 📚 **Course Connection:** Config loading = the OS `/etc` configuration hierarchy. Just as Linux reads `/etc/fstab` (mount table), `/etc/passwd` (user table), and `/etc/resolv.conf` (DNS config) during startup, Claude Code's migration system ensures config file formats stay consistent with the current code version.
>
> **🔑 OS Analogy:** Just like your phone automatically migrating data during a system upgrade—the new version converts old-format contacts, photo libraries, and settings into the new format.

---

## 4. Initialization Stage: Subsystem Boot

> 📚 **Course Connection:** Permission initialization = establishing userland privileges in an OS. Just as the Linux kernel verifies user identity via PAM modules and reads `/etc/sudoers` to determine scope after boot, Claude Code's `init` stage must complete OAuth verification, keychain reads, and policy limit loading—determining "what this user is allowed to do."

The `init()` function in `entrypoints/init.ts` (line 57) is the system's "kernel initialization," wrapped with `memoize` to ensure it runs only once. It starts all subsystems in a precise order:

```
init()  ← memoized, runs only once
  │
  ├── 1. enableConfigs()
  │   └── Validate and enable the config system
  │
  ├── 2. applySafeConfigEnvironmentVariables()
  │   └── Set safe environment variables before the trust dialog
  │
  ├── 3. applyExtraCACertsFromConfig()
  │   └── Add extra CA certificates (must happen before the first TLS connection)
  │
  ├── 4. setupGracefulShutdown()
  │   └── Register exit handlers
  │
  ├── 5. Launch async background tasks (do not await completion)
  │   ├── 1P event log initialization (dynamic import, lazy-loads OTel sdk-logs)
  │   ├── OAuth account info fetch
  │   ├── JetBrains IDE detection
  │   ├── Git repository detection (detectCurrentRepository)
  │   ├── Remote settings loading promise initialization (only if isEligibleForRemoteManagedSettings())
  │   ├── Policy limits loading promise initialization (only if isPolicyLimitsEligible())
  │   └── Record first startup time
  │   Note: Remote settings and policy limits only **initialize loading promises** here;
  │   the actual loadRemoteManagedSettings() / loadPolicyLimits() calls happen
  │   in the preAction hook after init().
  │
  ├── 6. configureGlobalMTLS()
  │   └── Configure global mTLS (mutual TLS)
  │
  ├── 7. configureGlobalAgents()
  │   └── Configure HTTP proxies and mTLS agents
  │
  ├── 8. preconnectAnthropicApi()  ← Key optimization
  │   └── Establish TCP + TLS connection early (overlaps with subsequent steps)
  │
  ├── 9. initUpstreamProxy() (CCR mode, feature-gated)
  │   └── Start CONNECT relay for credential injection
  │
  └── 10. ensureScratchpadDir()
      └── Create the Scratchpad directory (for Agent communication)
```

### Careful Dependency Ordering

The initialization order is not arbitrary:
- **Config must be enabled first** (steps 1–2)—all subsequent subsystems need to read config
- **CA certificates before TLS** (step 3)—must configure the certificate chain before any network connection
- **Multiple async tasks launched in parallel** (step 5)—not awaited, running in the background "while the user is still looking at the UI." Remote settings and policy limit loading promises are triggered conditionally. 📚 *Course Connection: Parallel initialization = the `Promise.all` concurrency pattern—multiple independent I/O operations are initiated simultaneously, and any completed result can be used without serial waiting.*
- **mTLS before API preconnection** (steps 6–7 → 8)—configure TLS first, then establish the connection
- **`preconnectAnthropicApi()`** establishes the TCP + TLS connection to the Anthropic API during `init`, so the first API call does not need to wait for handshaking—a common HTTP client preconnection technique

---

## 5. Ready Stage: Deferred Prefetching

After `launchRepl()` renders the first screen (the user sees the input box), the system launches a batch of **deferred prefetches** in the background:

```
startDeferredPrefetches()
  ├── initUser()              ← Fetch user info
  ├── getUserContext()         ← User context (includes CLAUDE.md)
  ├── prefetchSystemContextIfSafe()  ← System context including getGitStatus() (5 parallel git operations)
  ├── getRelevantTips()        ← Tips and hints
  ├── countFilesRoundedRg()    ← File count statistics
  ├── refreshModelCapabilities()  ← Model capability cache refresh
  ├── settingsChangeDetector   ← Settings change listener
  └── skillChangeDetector      ← Skill change listener
```

**Design philosophy:** This information is not required for first-screen rendering, but it is needed for the first AI call. Placing it in the prefetch window "while the user is still looking at the UI / typing" means that by the time the user actually presses Enter, the information is already prepared.

### The 5 Parallel Git Commands for Git Status

Obtaining git status is not a single `git status`—`getGitStatus()` in `context.ts` runs 5 operations in parallel via `Promise.all`:

```
Executed in parallel (Promise.all):
  getBranch()                              ← Current branch (read via gitFilesystem cache layer)
  getDefaultBranch()                       ← Default branch (same, via cache layer)
  git --no-optional-locks status --short   ← Changed files list
  git --no-optional-locks log --oneline -n 5  ← Last 5 commits
  git config user.name                     ← Current git username
```

Note that `getBranch()` and `getDefaultBranch()` do not directly spawn git subprocesses; instead, they read `.git` directory files directly through the `gitFilesystem.ts` cache layer—faster than spawning a subprocess. The `--no-optional-locks` flag avoids acquiring `.git/index.lock` on read-only queries, preventing conflicts with other git operations.

Why parallel? Because git commands need to read the `.git` directory or spawn subprocesses—in large repositories this can be noticeably slow. Five sequential operations might take 500 ms; in parallel they might take only 150 ms.

---

## 6. Complete Timeline

```
t=0ms     User types the `claude` command
          ↓
          Node.js begins parsing main.tsx
          ├── startMdmRawRead() launched ── I/O parallel ──┐
          ├── startKeychainPrefetch() launched ──────────────┤
          └── Begins loading import modules                  │
                                                           │
t=~135ms  Module loading completes                          │
          MDM config read ← ──── may already be done ──────┘
          Keychain prefetch ← ──── may already be done ────┘
          ↓
          main() begins execution
          ├── Mode routing (Commander parses CLI args)
          └── preAction hook fires
                ├── await MDM + keychain prefetch completion
                ├── init() initialization
                │     ├── Load config
                │     ├── CA certs + mTLS + proxy
                │     ├── API preconnection
                │     └── Async background tasks (event logs, OAuth...)
                ├── runMigrations() (on first run / upgrade)
                └── loadRemoteManagedSettings() + loadPolicyLimits() (non-blocking)

t=~300ms  Initialization complete (estimated)
          ↓
          launchRepl() renders first screen
          ↓
          User sees the input box (can start typing)

t=~300ms+ Deferred prefetches run in the background (estimated)
          ├── User info + user context
          ├── Git status (5 parallel operations)
          ├── File count
          └── Model capability cache + change detectors

t=~500ms  Prefetching mostly complete
          ↓
          System fully ready
```

**From typing the command to seeing the input box: ~300 ms (estimated; no precise benchmark data is publicly available—`profileCheckpoint` instrumentation exists in the source but benchmark results have not been published).** For reference: Go-compiled `gh CLI` startup is virtually instantaneous (<50 ms), while Python's Aider cold start takes roughly 1–2 s. Claude Code's ~300 ms is reasonable for a heavyweight Node.js CLI—Pre-import I/O parallelism and deferred prefetching do reduce perceived latency, but the ~135 ms module loading overhead is a floor that cannot be avoided, an inherent cost of JS/TS runtime CLI tools.

---

## 7. Design Trade-offs

### Effective Engineering Decisions

1. **Pre-import I/O parallelism**—stuffing work into unavoidable wait time (module loading) is a pragmatic optimization; unconventional but clearly effective
2. **Deferred prefetching design** pushes non-urgent I/O into the "user typing" window—does not block first-screen rendering, a common perceived-performance optimization in CLI tools
3. **5 parallel git operations** instead of serial queries—standard I/O parallelization practice, and `getBranch()`/`getDefaultBranch()` avoid spawning subprocesses via the filesystem cache layer
4. **Idempotent migration design**—crash-safe, rerunning causes no errors, standard practice from the database migration world
5. **preAction hook pattern**—via Commander.js's `preAction` hook, pure-info commands like `claude --help` do not trigger `init()`, avoiding unnecessary initialization overhead (keychain reads, network connections, etc.)
6. **OpenTelemetry lazy loading**—the ~400 KB OTel + protobuf modules are deferred via dynamic `import()` until telemetry actually initializes; the gRPC exporter (~700 KB `@grpc/grpc-js`) is further lazy-loaded only when the gRPC protocol is selected. In total, ~1.1 MB of telemetry dependencies are off the critical startup path
7. **Multiple modes routed through a single entry**—reduces the number of entry files, but centralizes complexity

### Costs and Limitations

1. **Pre-import side effects** violate the JavaScript convention of "imports should have no side effects"—may confuse developers unfamiliar with the rationale. The source marks this as intentional with `eslint-disable custom-rules/no-top-level-side-effects`
2. **~135 ms module loading time** indicates a large dependency tree—the import chain of 1,884 files is unavoidable
3. **Branch logic for multiple modes** concentrated in one function—adding new modes in the future may make `main()` very long
4. **Deferred prefetching time-window assumption**—if the user types extremely fast (e.g., pasting text and immediately pressing Enter), prefetching may not finish in time
5. **Migration version number is hardcoded**—no auto-discovery mechanism; each migration requires manually incrementing the version number

---

## 8. Code Landing Points

Here are the precise source locations for the key concepts in this chapter:

| Concept | File | Line | Explanation |
|---------|------|------|-------------|
| Pre-import side effects | `src/main.tsx` | :9-20 | `profileCheckpoint` + `startMdmRawRead` + `startKeychainPrefetch` interleaved and executed between imports |
| preAction hook | `src/main.tsx` | :907-967 | Commander's `preAction` hook—awaits MDM/keychain → `init()` → `runMigrations()` → `loadRemoteManagedSettings()` + `loadPolicyLimits()` |
| `init()` main function | `src/entrypoints/init.ts` | :57 | Wrapped with `memoize` to ensure single execution; starts all subsystems in order |
| CA certificate config | `src/entrypoints/init.ts` | :77-79 | `applyExtraCACertsFromConfig()`—must complete before the first TLS handshake (Bun's BoringSSL caching mechanism) |
| 7 async background tasks | `src/entrypoints/init.ts` | :94-132 | 1P event logs, OAuth, JetBrains detection, git repo detection, remote settings, etc. launched in parallel |
| API preconnection | `src/entrypoints/init.ts` | :153-155 | `preconnectAnthropicApi()`—TCP+TLS handshake overlaps with subsequent steps |

---

> **[Chart placeholder 2.2-A]**: Boot timeline — time distribution across the three stages (Pre-import / Init / Ready)
> **[Chart placeholder 2.2-B]**: Mode routing decision tree — mapping from CLI args / env vars / feature flags to runtime modes
> **[Chart placeholder 2.2-C]**: I/O parallelism Gantt chart — parallel timeline of module loading / MDM read / keychain prefetch / deferred prefetch
