The Startup Pipeline

What happens from typing claude to the first user interaction. The startup sequence is heavily optimized for latency — fast-path routing avoids loading the 785KB main bundle for simple commands, and parallel subprocess spawning overlaps with import loading to shave ~200ms off startup.

Phase 1: Fast-path routing (microseconds)

src/entrypoints/cli.tsx is the true first code to run. Before loading the heavy main bundle, it checks process.argv for commands that can exit immediately:

Command What happens Cost
--version Prints MACRO.VERSION (build-time inlined), exits Zero module loading
--dump-system-prompt Internal Anthropic path Minimal loading
remote-control / rc Bridge mode Separate code path
daemon Daemon supervisor Separate code path
ps / logs / attach / kill Session management Separate code path

Only commands that need the full agent system fall through to main.tsx.

Phase 2: Parallel prefetch (during imports, ~135ms)

While the main bundle's imports are still loading, main.tsx fires parallel subprocesses:

  1. startMdmRawRead() — spawns MDM subprocesses (plutil on macOS, reg query on Windows) to load managed enterprise settings
  2. startKeychainPrefetch() — fires macOS keychain reads for OAuth tokens and legacy API keys (~65ms saved by parallelizing)

These run concurrently with the ~135ms of import resolution, so their latency is mostly hidden.

Phase 3: Initialization (init.ts, memoized — runs once)

The init() function configures external integrations in a specific order. The ordering is critical: network must be configured before API calls, config before feature flags, TLS certs before first handshake.

1. enableConfigs()                    — load and validate settings.json files
2. applySafeConfigEnvironmentVariables() — env vars before trust dialog
3. applyExtraCACertsFromConfig()      — TLS certs (Bun caches TLS store at boot)
4. setupGracefulShutdown()            — register cleanup handlers
5. [parallel async]:
   - initialize1PEventLogging()       — fire-and-forget
   - populateOAuthAccountInfoIfNeeded() — fire-and-forget
   - initJetBrainsDetection()         — cache population
   - detectCurrentRepository()        — git repo detection
6. initializeRemoteManagedSettingsLoadingPromise() — non-blocking
7. initializePolicyLimitsLoadingPromise()          — non-blocking
8. preconnectAnthropicApi()           — TCP+TLS handshake warmup (~100-200ms saved)
9. configureGlobalMTLS()              — mutual TLS
10. configureGlobalAgents()           — proxy config
11. setShellIfWindows()               — git-bash setup

Telemetry initialization is deferred to a separate function (initializeTelemetryAfterTrust()) that lazy-loads ~400KB of OpenTelemetry + protobuf, with gRPC exporters (~700KB) further deferred. Total: ~1.1MB of code kept out of the startup path.

Phase 4: Commander.js preAction hook

Before any command action, a preAction hook runs:

  1. ensureMdmSettingsLoaded() — await the parallel MDM subprocess from Phase 2
  2. ensureKeychainPrefetchCompleted() — await keychain reads from Phase 2
  3. init() — the memoized init from Phase 3
  4. initSinks() — analytics sink attachment
  5. setInlinePlugins() — if --plugin-dir provided
  6. runMigrations() — 12 migration steps, current version 11
  7. loadRemoteManagedSettings() + loadPolicyLimits() — non-blocking fire-and-forget

Phase 5: Config schema loading

Settings are loaded from 5 scopes with defined precedence:

userSettings → projectSettings → localSettings → flagSettings → policySettings

Each scope's settings.json is validated against SettingsSchema (Zod v4). Invalid fields are preserved in file but not applied (graceful degradation). Managed settings follow systemd drop-in convention: base file + managed-settings.d/*.json.

Phase 6: Setup (setup.ts, per-session)

  1. Set working directory (setCwd(cwd)) — must happen before hooks
  2. Capture hooks configuration snapshot
  3. Initialize FileChanged hook watcher
  4. Worktree creation (if --worktree flag)
  5. Background jobs (unless --bare):
  6. Session memory initialization
  7. Context collapse (feature-gated)
  8. Version locking
  9. Command loading, plugin hooks, attribution hooks
  10. Team memory watcher, release notes check
  11. Permission mode validation
  12. Telemetry event: tengu_started

Phase 7: Mode divergence

The main action handler determines the operating mode and diverges:

Mode Condition Handler
Interactive REPL No -p, TTY available launchRepl() — full Ink/React TUI
Headless/print -p flag or no TTY runHeadless() — single-shot execution
MCP server mcp serve subcommand startMCPServer() — tool exposure via MCP
SDK --sdk-url with stream-json Non-interactive, WebSocket control

The --bare flag

--bare sets CLAUDE_CODE_SIMPLE=1 and skips: hooks, LSP, plugins, attribution, auto-memory, background prefetches, keychain reads, CLAUDE.md auto-discovery. Only 3 tools available (Bash, FileRead, FileEdit). The fastest path to first interaction.

System prompt assembly

Before the first API call, the system prompt is assembled from: - Base prompt (model-specific) - CLAUDE.md files (project instructions, auto-discovered) - Memory files (auto-memory) - Git status snapshot - Tool descriptions (sorted for cache stability) - System reminders - Feature-specific additions (coordinator mode, kairos, etc.)

The DANGEROUS_ prefix convention marks prompt sections that are security-sensitive and should never be cached in a way that could be shared across sessions.

Timeline summary

t=0ms     cli.tsx: fast-path check (--version exits here)
t=0ms     Start MDM subprocess + keychain prefetch (parallel)
t=0-135ms main.tsx imports loading (parallel with prefetch)
t=135ms   init(): config → TLS → graceful shutdown → parallel async
t=135ms   preconnectAnthropicApi(): TCP+TLS warmup (parallel)
t=200ms   Commander.js: await prefetch results, run migrations
t=250ms   Setup: CWD, hooks, background jobs
t=300ms   Mode divergence: REPL / headless / MCP / SDK
t=350ms+  System prompt assembly, first API call

Actual time varies by platform and whether enterprise settings (MDM, remote managed) are involved.