claude 키 입력부터 인터랙티브 REPL까지 — 시작의 모든 단계를 심층 분석합니다.
터미널에 claude를 입력하면 첫 번째 프롬프트가 표시되기 전에 다단계 부트 파이프라인이 실행됩니다. 이 파이프라인을 이해하면 지연 시간의 원인을 추론하고, 시작 관련 이상 현상을 디버깅하며, 낮은 TTI(Time-to-Interactive) 지표를 유지하는 내장 병렬 처리 구조를 파악할 수 있습니다.
entrypoints/cli.tsx → main.tsx → setup.ts → bootstrap/state.ts → replLauncher.tsx → ink.ts
세 개의 중첩 레이어로 구성됩니다:
cli.tsx가 무비용 패스트 패스, 환경 준비, argv 디스패치를 처리main.tsx가 Commander 파싱, 초기화, 마이그레이션, 권한 확인을 관리setup.ts + replLauncher.tsx가 세션 배선과 Ink 렌더링을 담당프로세스 진입부터 첫 렌더링까지의 호출 시퀀스를 추적하는 종합 플로우차트입니다.
cli.tsx)의도적으로 가볍게 설계된 부트스트랩으로, 동적 임포트를 활용합니다. --version, --daemon-worker, --claude-in-chrome-mcp 같은 패스트 패스는 무거운 CLI 표면을 로드하지 않고 바로 반환됩니다.
// cli.tsx — 패스트 패스: --version은 임포트가 전혀 필요 없음
const args = process.argv.slice(2)
if (args.length === 1 && (args[0] === '--version' || args[0] === '-v')) {
console.log(`${MACRO.VERSION} (Claude Code)`)
return
}
// 다른 모든 경로는 먼저 시작 프로파일러를 로드
const { profileCheckpoint } = await import('../utils/startupProfiler.js')
profileCheckpoint('cli_entry')
feature('X') 호출은 Bun의 데드 코드 제거를 활용하는 빌드 타임 플래그입니다. BRIDGE_MODE, DAEMON, SSH_REMOTE 같은 기능은 외부 빌드에서 완전히 제거될 수 있습니다. 디스패치 테이블은 개방-폐쇄 원칙을 따라 main.tsx를 수정하지 않고도 새로운 패스트 패스를 추가할 수 있습니다.
환경 변수 변경은 모듈 평가 전에 수행됩니다: COREPACK 피닝이 비활성화되고, CCR 컨테이너는 NODE_OPTIONS를 통해 8GB 힙 제한을 받습니다.
main.tsx 최상위)다른 임포트 전에 세 가지 사이드 이펙트가 실행됩니다:
// 이 사이드 이펙트들은 다른 모든 임포트 전에 실행되어야 함:
profileCheckpoint('main_tsx_entry') // 타임스탬프: 모듈 평가 시작
startMdmRawRead() // plutil/reg query 서브프로세스를 병렬 실행
startKeychainPrefetch() // macOS 키체인 읽기 시작 (OAuth + API 키)
약 135ms의 정적 임포트가 로드되는 동안 MDM 정책과 키체인 읽기가 병렬로 실행됩니다.
macOS의 MDM(모바일 디바이스 관리)은 defaults 도메인에 엔터프라이즈 정책을 plutil(macOS) 또는 reg query(Windows)를 통해 저장합니다. 서브프로세스마다 약 20~40ms가 소요됩니다.
init() 내부의 applySafeConfigEnvironmentVariables()가 관리 설정을 적용하기 전에 MDM 정책이 필요합니다. 모듈 평가 시점에 startMdmRawRead()를 실행하면 서브프로세스가 임포트와 동시에 실행되어, init()이 ensureMdmSettingsLoaded()를 호출할 때 결과가 이미 캐시되어 있습니다.
마찬가지로 startKeychainPrefetch()는 OAuth 토큰과 레거시 API 키를 위한 두 개의 비동기 macOS 키체인 읽기를 실행합니다. 이것 없이는 동기식 spawn으로 순차 읽기가 되어 macOS 시작 시마다 약 65ms가 추가됩니다.
임포트가 로드된 후 main()이 Commander 실행 전에 --settings와 --setting-sources 플래그를 위한 eagerLoadSettings()를 호출한 다음 Commander의 .parse()를 실행합니다. Commander는 cwd, permissionMode, --print/-p 모드, --model, --resume, --session, MCP 서버 구성 등을 해석합니다.
// main.tsx — 설정 버전 범프마다 한 번 마이그레이션 실행
const CURRENT_MIGRATION_VERSION = 11
function runMigrations(): void {
if (getGlobalConfig().migrationVersion !== CURRENT_MIGRATION_VERSION) {
migrateAutoUpdatesToSettings()
migrateSonnet45ToSonnet46() // 예: 모델 문자열 업그레이드
migrateOpusToOpus1m()
// ...8개 추가 마이그레이션 함수...
saveGlobalConfig(prev => ({ ...prev, migrationVersion: CURRENT_MIGRATION_VERSION }))
}
}
마이그레이션은 프로세스 시작 시마다 실행되지만 migrationVersion으로 게이팅됩니다. 다운그레이드하면 이미 올라간 마이그레이션 버전이 남아 재실행을 방지하여 미묘한 설정 불일치가 발생할 수 있습니다.
setup() (setup.ts)setup()은 신중하게 정해진 순서로 세션을 배선합니다:
switchSession()을 통한 선택적 커스텀 세션 IDsetCwd(cwd) — cwd를 읽는 코드보다 반드시 먼저 실행.claude/settings.json 읽기initSessionMemory(), getCommands() 프리페치, 플러그인 훅initSinks() — 분석 + 오류 싱크 연결, 대기열 이벤트 드레인logEvent('tengu_started') — 최초의 신뢰할 수 있는 "프로세스 시작" 비컨projectConfig에서 이전 세션 종료 메트릭 로깅// setup.ts — setCwd 순서 주석 (소스에서 그대로)
// 중요: setCwd()는 cwd에 의존하는 다른 코드보다 먼저 호출되어야 합니다
setCwd(cwd)
// 중요: setCwd() 이후에 호출해야 올바른 디렉토리에서 훅이 로드됩니다
captureHooksConfigSnapshot()
tengu_started 비컨소스 주석에 정확한 배치 이유가 설명되어 있습니다:
"세션 성공률 분모. 분석 싱크가 연결된 직후에 발생시킨다 — 어떤 파싱, 페칭, 또는 I/O보다 먼저. 이 비컨은 릴리스 건강 모니터링을 위한 가장 이른 신뢰할 수 있는 '프로세스 시작' 신호이다."
checkForReleaseNotes() 크래시로 후속 이벤트가 무효화된 인시던트 inc-3694를 참조합니다. 비컨 배치는 다운스트림 코드에서 예외가 발생하더라도 분모가 기록되도록 보장합니다.
--bare / CLAUDE_CODE_SIMPLE)!isBareMode()로 보호되는 단계들:
startDeferredPrefetches())설계 원칙: bare 모드는 지연 시간에 민감합니다. CI 파이프라인에서 매일 수백 번 Claude를 호출할 때 밀리초 단위의 절감이 중요합니다.
--worktree가 전달되면 setup()은 다른 파일시스템 접근 전에 git worktree를 생성합니다:
getPlanSlug() 또는 PR 번호에서 슬러그 생성createWorktreeForSession() 호출 — 구성된 경우 WorktreeCreate 훅에 위임setCwd(worktreePath) 및 setProjectRoot() 호출clearMemoryFileCaches() 호출.claude/settings.json에서 훅 설정 재캡처setProjectRoot()는 세션 기간 동안 프로젝트 정체성(세션 히스토리, 스킬, CLAUDE.md)을 원래 레포 루트가 아닌 worktree 루트로 고정합니다.
bootstrap/state.ts)state.ts는 세션 범위 전역 상태의 단일 진실 소스입니다. 상단 주석: "여기에 더 많은 상태를 추가하지 마세요 — 전역 상태에 신중하세요."
추적되는 상태 범주:
sessionId, originalCwd, projectRoot, cwdtotalCostUSD, modelUsage, 토큰 카운터, FPS 메트릭isInteractive, sessionBypassPermissionsMode, isRemoteModemeter, loggerProvider, tracerProviderpromptCache1hEligible, afkModeHeaderLatched, fastModeHeaderLatchedregisteredHooks, invokedSkills, sessionCronTasks// bootstrap/state.ts — 초기 상태 팩토리 (간소화 발췌)
function getInitialState(): State {
let resolvedCwd = ''
try {
// 심볼릭 링크를 해석하여 세션 저장 경로를 일관되게 유지
resolvedCwd = realpathSync(cwd())
} catch { resolvedCwd = cwd() }
return {
originalCwd: resolvedCwd,
projectRoot: resolvedCwd,
sessionId: asSessionId(randomUUID()),
isInteractive: true,
totalCostUSD: 0,
// ... 약 60개 추가 필드
}
}
afkModeHeaderLatched, fastModeHeaderLatched, thinkingClearLatched 같은 필드는 Anthropic API 프롬프트 캐시 헤더를 세션 내내 안정적으로 유지합니다. 한번 활성화되면 사용자가 세션 중간에 설정을 토글해도 헤더가 계속 켜져 있습니다 — 헤더를 변경하면 서버 측의 비용이 큰 캐시가 무효화됩니다(약 50~70K 토큰 재처리).
replLauncher.tsx + ink.ts)마지막 단계에서 React 기반 TUI를 렌더링합니다. launchRepl()이 순환 임포트를 피하기 위해 App과 REPL 컴포넌트를 동적으로 임포트한 후 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는 모든 렌더 호출을 <ThemeProvider>로 자동 래핑하여 ThemedBox와 ThemedText 컴포넌트가 테마 컨텍스트를 별도로 마운트하지 않아도 동작합니다:
// ink.ts — 모든 렌더를 ThemeProvider로 래핑
function withTheme(node: ReactNode): ReactNode {
return createElement(ThemeProvider, null, node)
}
export async function render(node, options) {
return inkRender(withTheme(node), options)
}
첫 렌더링 이후: startDeferredPrefetches()가 첫 렌더링에 필요하지 않은 백그라운드 작업을 시작합니다: initUser(), getUserContext(), MCP URL 프리페치, 모델 기능 새로고침, 파일 변경 감지기 초기화. 이 작업은 사용자가 첫 메시지를 입력하는 동안 실행됩니다 — 인간 반응 시간 윈도우를 활용하여 숨겨집니다.
cli.tsx의 패스트 패스는 무거운 모듈을 전혀 로드하지 않고 종료합니다; claude --version은 main.tsx를 건드리지 않습니다setCwd()는 captureHooksConfigSnapshot()보다 반드시 먼저 실행해야 합니다; 위반하면 잘못된 훅 설정이 로드됩니다--bare)는 스크립트/SDK 사용 사례를 위해 불필요한 시작 단계를 모두 제거합니다bootstrap/state.ts는 전역 상태 원장입니다; 프롬프트 캐시 래치 필드는 서버 측 캐시를 보호하기 위해 API 헤더를 안정적으로 유지합니다tengu_started 이벤트는 가장 이른 신뢰할 수 있는 비컨입니다; initSinks() 이후의 모든 것이 세션 성공률에 포함됩니다Q1. cli.tsx 상단의 패스트 패스 체크의 주된 목적은 무엇인가요?
cli.tsx의 패스트 패스는 동적 임포트를 사용하여 --version, --daemon-worker, remote-control 같은 서브커맨드가 무거운 main.tsx 모듈 그래프를 전혀 임포트하지 않고 종료됩니다.Q2. startMdmRawRead()와 startKeychainPrefetch()가 다른 임포트 전에 main.tsx 최상위에서 사이드 이펙트로 호출되는 이유는?
plutil 경유)와 키체인 읽기는 20~65ms가 소요됩니다. 모듈 평가 중에 실행하면 임포트 체인과 동시에 실행되어 init()이 필요로 할 때 이미 해결되어 있습니다. 순차 읽기는 크리티컬 패스에 해당 지연 시간을 추가할 것입니다.Q3. setup.ts에서 setCwd(cwd)가 captureHooksConfigSnapshot()보다 먼저 호출되어야 하는 이유는?
setCwd() 이후에 호출해야 올바른 디렉토리에서 훅이 로드됩니다." captureHooksConfigSnapshot()은 프로젝트의 설정 파일을 읽어 구성된 훅의 스냅샷을 찍습니다 — cwd가 의도한 프로젝트 루트가 아닌 셸의 작업 디렉토리인 경우 잘못된 훅이 로드됩니다.Q4. "bare 모드"(--bare / CLAUDE_CODE_SIMPLE)는 부트 중 무엇을 건너뛰나요?
Q5. bootstrap/state.ts에 afkModeHeaderLatched와 fastModeHeaderLatched 같은 필드가 존재하는 이유는?
state.ts의 주석에 이것들이 '스티키-온 래치'임이 설명되어 있습니다. AFK 모드, 빠른 모드, 또는 캐시 편집 모드가 처음 활성화되면 해당 API 헤더가 나머지 세션 동안 유지됩니다. 헤더가 GrowthBook/설정 변경마다 토글되면 서버의 캐시된 프롬프트 접두사가 무효화되어 비용이 큰 캐시 미스가 발생합니다(Anthropic 측에서 약 50~70K 토큰 재처리).