Entry Points
Claude Code has three main entry points that form a layered bootstrap sequence: cli.tsx (fast-path dispatch), init.ts (configuration and environment setup), and main.tsx (full application startup). Each layer is designed to defer expensive work until it is actually needed.
cli.tsx: Fast-Path Bootstrap
entrypoints/cli.tsx is the process entry point. Its primary job is to handle fast-path flags that can exit immediately without loading the full application. All imports within cli.tsx are dynamic (await import(...)) to minimize module evaluation for these fast paths.
The fast-path for --version has zero imports beyond the entry file itself. It reads MACRO.VERSION (inlined at build time) and exits, making version checks near-instantaneous.
Fast-Path Checks
The following checks are evaluated in order. If any match, the process exits without reaching init.ts or main.tsx:
-
--version/-v/-V: Prints the version string and exits. No module loading at all. -
--dump-system-prompt: Renders the full system prompt to stdout and exits. Used by prompt sensitivity evaluations. Feature-gated (DUMP_SYSTEM_PROMPT) and eliminated from external builds. -
--claude-in-chrome-mcp: Launches the Chrome integration MCP server. Runs as a long-lived process serving MCP protocol over stdio. -
--chrome-native-host: Launches the Chrome native messaging host for browser extension communication. -
--computer-use-mcp: Launches the computer-use MCP server. Feature-gated (CHICAGO_MCP). -
--daemon-worker=<kind>: Spawns a daemon worker process. Feature-gated (DAEMON). These are lean workers with no config or analytics initialization. If a worker kind needs auth, it handles that internally. -
remote-control/rc/remote/sync/bridge: Launches bridge mode, serving the local machine as a remote environment. Feature-gated (BRIDGE_MODE), requires OAuth authentication and a GrowthBook gate check.
For all other invocations, cli.tsx dynamically imports and calls into main.tsx.
Pre-Boot Environment Setup
Before any fast-path checks, cli.tsx performs two environment-level side effects:
// Prevent corepack from auto-pinning yarnpkg into package.json
process.env.COREPACK_ENABLE_AUTO_PIN = '0'
// Set max heap for child processes in remote container environments
if (process.env.CLAUDE_CODE_REMOTE === 'true') {
process.env.NODE_OPTIONS = '--max-old-space-size=8192'
}init.ts: Configuration & Environment
entrypoints/init.ts handles one-time initialization that must complete before the main application logic runs. It is wrapped in memoize() so it executes at most once per process.
Initialization Sequence
export const init = memoize(async (): Promise<void> => {
// 1. Enable and validate configuration system
enableConfigs()
// 2. Apply safe environment variables (before trust dialog)
applySafeConfigEnvironmentVariables()
// 3. Inject custom CA certificates (must happen before first TLS handshake)
applyExtraCACertsFromConfig()
// 4. Register process shutdown handlers
setupGracefulShutdown()
// 5. Initialize 1P event logging (deferred via dynamic import)
// 6. Configure global HTTP agents (proxy support)
// 7. Configure mTLS if applicable
// 8. Set up platform-specific shell paths (Windows)
// ...
})Key design decisions in init.ts:
-
Safe vs full env vars: Environment variables are applied in two phases. "Safe" variables are applied before the trust dialog (so proxy settings work for the trust check itself). Full variables are applied later in
main.tsxafter trust is established. -
Deferred telemetry: OpenTelemetry initialization is deferred via
import()to avoid loading ~400KB of modules at startup. The gRPC exporters add another ~700KB that is further lazy-loaded only when telemetry is actually initialized. -
Memoization:
init()is idempotent vialodash-es/memoize. Both the interactive and headless paths call it, but the work only happens once.
The applyExtraCACertsFromConfig() call must happen before any TLS connection. Bun caches the TLS certificate store at boot via BoringSSL, so custom CA certs injected after the first handshake would be ignored.
main.tsx: Full Application Startup
main.tsx is where the heavy lifting happens. It defines the Commander.js command structure and orchestrates the full boot sequence after init() completes.
Top-Level Side Effects
The first three lines of main.tsx (before any standard imports) fire parallel side effects:
profileCheckpoint('main_tsx_entry') // Mark entry for startup profiler
startMdmRawRead() // Fire MDM subprocesses (plutil/reg query)
// Runs in parallel with ~135ms of imports
startKeychainPrefetch() // Fire macOS keychain reads for OAuth +
// legacy API key in parallel (~65ms saved)Boot Phases in main.tsx
After imports and init():
- MDM Settings:
ensureMdmSettingsLoaded()waits for the MDM subprocess started at the top of the file - Keychain Completion:
ensureKeychainPrefetchCompleted()awaits the parallel keychain reads - Settings Migrations: Runs a series of migrations (model renames like Fennec to Opus, permission migrations, auto-updater settings)
- File System Setup: Resolves git root, working directory, worktree paths
- Prefetch: Fires non-blocking prefetches for bootstrap data, GrowthBook, MCP registry, credentials
- Mode Branching: Enters interactive or headless mode
Execution Modes
Interactive mode is the default when Claude Code is launched without the -p flag. It renders a full terminal UI using Ink (React for the terminal).
main.tsx
|
v
showSetupScreens() : Trust dialog, onboarding, model deprecation warnings
|
v
renderAndRun() : Mount Ink/React component tree
|
v
REPL Component : Main interaction loop
|-- PromptInput : User input with autocomplete, slash commands
|-- MessageList : Streaming message display
|-- StatusBar : Model, cost, token usage
|-- Footer : Tasks, MCP status, bridge indicatorsThe REPL handles:
- User input parsing and slash-command dispatch
- Message queue management for multi-message sequences
- Tool permission prompts (approve/deny UI)
- Auto-compaction triggers
- Session persistence and resume
SDK Entry Point
For programmatic usage, Claude Code exposes a QueryEngine class that can be instantiated directly without going through the CLI bootstrap:
import { QueryEngine } from './QueryEngine.js'
const engine = new QueryEngine({
cwd: '/path/to/project',
tools: getTools(),
commands: getCommands(),
mcpClients: [],
agents: [],
canUseTool: myPermissionHandler,
getAppState: () => appState,
setAppState: (fn) => { appState = fn(appState) },
// ...additional config
})
for await (const event of engine.submitMessage('Fix the failing tests')) {
// Handle SDKMessage events
}The QueryEngine manages:
- Conversation history across multiple
submitMessage()calls - File state cache for tracking file reads
- Cumulative API usage and cost
- Permission denial tracking
- Abort controller for cancellation
The SDK path skips all UI-related initialization (Ink, React, terminal detection) and defers MCP server connections until the first query that needs them. This makes programmatic startup significantly faster than the interactive path.