Claude Code 부트 시퀀스

claude 키 입력부터 인터랙티브 REPL까지 — 시작의 모든 단계를 심층 분석합니다.

01

개요

터미널에 claude를 입력하면 첫 번째 프롬프트가 표시되기 전에 다단계 부트 파이프라인이 실행됩니다. 이 파이프라인을 이해하면 지연 시간의 원인을 추론하고, 시작 관련 이상 현상을 디버깅하며, 낮은 TTI(Time-to-Interactive) 지표를 유지하는 내장 병렬 처리 구조를 파악할 수 있습니다.

다루는 소스 파일: entrypoints/cli.tsxmain.tsxsetup.tsbootstrap/state.tsreplLauncher.tsxink.ts

세 개의 중첩 레이어로 구성됩니다:

02

부트 파이프라인 플로우차트

프로세스 진입부터 첫 렌더링까지의 호출 시퀀스를 추적하는 종합 플로우차트입니다.

flowchart TD A["프로세스 시작"] --> B["COREPACK_ENABLE_AUTO_PIN=0 설정"] B --> C{"패스트 패스 체크"} C -->|"--version/-v"| D["버전 출력 후 종료"] C -->|"--daemon-worker"| E["데몬 워커 실행"] C -->|"remote-control/bridge"| F["브릿지 모드"] C -->|"기본 경로"| G["profileCheckpoint → main.tsx 동적 임포트"] G --> H["startMdmRawRead() 병렬 실행"] G --> I["startKeychainPrefetch() 병렬 실행"] G --> J["정적 임포트 로드 (~135ms)"] H & I & J --> K["Commander.parse() 호출"] K --> L["init() 실행"] L --> M["마이그레이션 실행"] M --> N["setup() 실행"] N --> O["Node.js ≥18 확인"] O --> P{"isBareMode?"} P -->|"아니오"| Q["UDS 메시징 + 팀메이트 스냅샷"] P -->|"예"| R["최소 설정"] Q --> S["setCwd() + 훅 설정 스냅샷 캡처"] R --> S S --> T{"--worktree 플래그?"} T -->|"예"| U["createWorktreeForSession()"] T -->|"아니오"| V["백그라운드 작업 시작"] U --> V V --> W["initSinks() + 'tengu_started' 이벤트 기록"] W --> X["권한 안전 검사"] X --> Y["REPL 실행"] Y --> Z["App + REPL 임포트 → renderAndRun()"] Z --> AA["첫 렌더링 — 사용자가 프롬프트를 봄"] AA --> AB["startDeferredPrefetches() 백그라운드 실행"] style A fill:#c47a50,color:#1a1816 style AA fill:#6e9468,color:#1a1816 style D fill:#7d9ab8,color:#1a1816 style E fill:#7d9ab8,color:#1a1816 style F fill:#7d9ab8,color:#1a1816
03

단계별 상세 분석

Phase 1 — CLI 진입점 (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 힙 제한을 받습니다.

Phase 2 — 병렬 프리페치 사이드 이펙트 (main.tsx 최상위)

다른 임포트 전에 세 가지 사이드 이펙트가 실행됩니다:

// 이 사이드 이펙트들은 다른 모든 임포트 전에 실행되어야 함:
profileCheckpoint('main_tsx_entry')   // 타임스탬프: 모듈 평가 시작

startMdmRawRead()     // plutil/reg query 서브프로세스를 병렬 실행
startKeychainPrefetch() // macOS 키체인 읽기 시작 (OAuth + API 키)

약 135ms의 정적 임포트가 로드되는 동안 MDM 정책과 키체인 읽기가 병렬로 실행됩니다.

딥 다이브 — 왜 MDM 읽기를 이렇게 일찍 실행하나?

macOS의 MDM(모바일 디바이스 관리)은 defaults 도메인에 엔터프라이즈 정책을 plutil(macOS) 또는 reg query(Windows)를 통해 저장합니다. 서브프로세스마다 약 20~40ms가 소요됩니다.

init() 내부의 applySafeConfigEnvironmentVariables()가 관리 설정을 적용하기 전에 MDM 정책이 필요합니다. 모듈 평가 시점에 startMdmRawRead()를 실행하면 서브프로세스가 임포트와 동시에 실행되어, init()ensureMdmSettingsLoaded()를 호출할 때 결과가 이미 캐시되어 있습니다.

마찬가지로 startKeychainPrefetch()는 OAuth 토큰과 레거시 API 키를 위한 두 개의 비동기 macOS 키체인 읽기를 실행합니다. 이것 없이는 동기식 spawn으로 순차 읽기가 되어 macOS 시작 시마다 약 65ms가 추가됩니다.

Phase 3 — Commander 인수 파싱

임포트가 로드된 후 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으로 게이팅됩니다. 다운그레이드하면 이미 올라간 마이그레이션 버전이 남아 재실행을 방지하여 미묘한 설정 불일치가 발생할 수 있습니다.

Phase 4 — setup() (setup.ts)

setup()은 신중하게 정해진 순서로 세션을 배선합니다:

  1. Node.js 버전 게이트 (≥18 필수)
  2. switchSession()을 통한 선택적 커스텀 세션 ID
  3. UDS(Unix Domain Socket) 메시징 서버 시작
  4. 팀메이트/스웜 스냅샷 (비bare 모드에서만)
  5. iTerm2 및 Terminal.app 백업 복원 (중단된 설정용)
  6. setCwd(cwd) — cwd를 읽는 코드보다 반드시 먼저 실행
  7. Hooks 설정 스냅샷 — 새 cwd에서 .claude/settings.json 읽기
  8. FileChanged 훅 감시기 초기화
  9. 선택적 worktree 생성 + tmux 세션
  10. 백그라운드 작업: initSessionMemory(), getCommands() 프리페치, 플러그인 훅
  11. initSinks() — 분석 + 오류 싱크 연결, 대기열 이벤트 드레인
  12. logEvent('tengu_started') — 최초의 신뢰할 수 있는 "프로세스 시작" 비컨
  13. API 키 프리페치 (안전 경로에서만)
  14. 릴리스 노트 확인 + 최근 활동 가져오기
  15. 권한 안전 검사 (root/sudo 가드, Docker 샌드박스 게이트)
  16. projectConfig에서 이전 세션 종료 메트릭 로깅
// setup.ts — setCwd 순서 주석 (소스에서 그대로)
// 중요: setCwd()는 cwd에 의존하는 다른 코드보다 먼저 호출되어야 합니다
setCwd(cwd)

// 중요: setCwd() 이후에 호출해야 올바른 디렉토리에서 훅이 로드됩니다
captureHooksConfigSnapshot()
딥 다이브 — tengu_started 비컨

소스 주석에 정확한 배치 이유가 설명되어 있습니다:

"세션 성공률 분모. 분석 싱크가 연결된 직후에 발생시킨다 — 어떤 파싱, 페칭, 또는 I/O보다 먼저. 이 비컨은 릴리스 건강 모니터링을 위한 가장 이른 신뢰할 수 있는 '프로세스 시작' 신호이다."

checkForReleaseNotes() 크래시로 후속 이벤트가 무효화된 인시던트 inc-3694를 참조합니다. 비컨 배치는 다운스트림 코드에서 예외가 발생하더라도 분모가 기록되도록 보장합니다.

딥 다이브 — Bare 모드 (--bare / CLAUDE_CODE_SIMPLE)

!isBareMode()로 보호되는 단계들:

  • UDS 메시징 서버 (훅 주입 없음)
  • 팀메이트 스냅샷 (스웜 미사용)
  • 세션 메모리 초기화
  • 플러그인 훅 사전 로드
  • 어트리뷰션 훅 + 레포 분류
  • 모든 지연 프리페치 (startDeferredPrefetches())

설계 원칙: bare 모드는 지연 시간에 민감합니다. CI 파이프라인에서 매일 수백 번 Claude를 호출할 때 밀리초 단위의 절감이 중요합니다.

딥 다이브 — Worktree + tmux 생성

--worktree가 전달되면 setup()은 다른 파일시스템 접근 전에 git worktree를 생성합니다:

  1. 정규 git root 해석 (기존 worktree 호출 처리)
  2. getPlanSlug() 또는 PR 번호에서 슬러그 생성
  3. createWorktreeForSession() 호출 — 구성된 경우 WorktreeCreate 훅에 위임
  4. 선택적으로 worktree 경로를 가리키는 tmux 세션 생성
  5. setCwd(worktreePath)setProjectRoot() 호출
  6. cwd가 변경되었으므로 clearMemoryFileCaches() 호출
  7. worktree의 .claude/settings.json에서 훅 설정 재캡처

setProjectRoot()는 세션 기간 동안 프로젝트 정체성(세션 히스토리, 스킬, CLAUDE.md)을 원래 레포 루트가 아닌 worktree 루트로 고정합니다.

Phase 5 — 전역 상태 (bootstrap/state.ts)

state.ts는 세션 범위 전역 상태의 단일 진실 소스입니다. 상단 주석: "여기에 더 많은 상태를 추가하지 마세요 — 전역 상태에 신중하세요."

추적되는 상태 범주:

// 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 토큰 재처리).

Phase 6 — Ink 렌더링 (replLauncher.tsx + ink.ts)

마지막 단계에서 React 기반 TUI를 렌더링합니다. launchRepl()이 순환 임포트를 피하기 위해 AppREPL 컴포넌트를 동적으로 임포트한 후 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>로 자동 래핑하여 ThemedBoxThemedText 컴포넌트가 테마 컨텍스트를 별도로 마운트하지 않아도 동작합니다:

// 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 프리페치, 모델 기능 새로고침, 파일 변경 감지기 초기화. 이 작업은 사용자가 첫 메시지를 입력하는 동안 실행됩니다 — 인간 반응 시간 윈도우를 활용하여 숨겨집니다.

04

핵심 요약

핵심 포인트

  • 부트 시퀀스는 세 개의 중첩 레이어로 구성됩니다: CLI 진입점 → main 함수 → setup + REPL 렌더링
  • cli.tsx의 패스트 패스는 무거운 모듈을 전혀 로드하지 않고 종료합니다; claude --versionmain.tsx를 건드리지 않습니다
  • MDM과 키체인 읽기는 모듈 평가 시점에 실행되어 약 135ms 임포트 체인과 병렬화됩니다 — 핵심 시작 지연 최적화
  • setCwd()captureHooksConfigSnapshot()보다 반드시 먼저 실행해야 합니다; 위반하면 잘못된 훅 설정이 로드됩니다
  • Bare 모드(--bare)는 스크립트/SDK 사용 사례를 위해 불필요한 시작 단계를 모두 제거합니다
  • bootstrap/state.ts는 전역 상태 원장입니다; 프롬프트 캐시 래치 필드는 서버 측 캐시를 보호하기 위해 API 헤더를 안정적으로 유지합니다
  • tengu_started 이벤트는 가장 이른 신뢰할 수 있는 비컨입니다; initSinks() 이후의 모든 것이 세션 성공률에 포함됩니다
  • 지연 프리페치는 첫 렌더링 후 실행되어 인간의 타이핑 시간에 숨겨집니다 — 아키텍처가 원시 지연 시간이 아닌 체감 지연 시간 중심으로 설계됨
05

지식 확인

퀴즈 — 5문제

Q1. cli.tsx 상단의 패스트 패스 체크의 주된 목적은 무엇인가요?

  • A) 전체 CLI를 로드하기 전에 사용자 인증을 검증하기 위해
  • B) main.tsx의 모듈을 전혀 로드하지 않고 특정 서브커맨드를 처리하기 위해
  • C) 진행하기 전에 Node.js 버전이 호환되는지 확인하기 위해
  • D) 이후 더 빠른 로드를 위해 모듈 캐시를 예열하기 위해
cli.tsx의 패스트 패스는 동적 임포트를 사용하여 --version, --daemon-worker, remote-control 같은 서브커맨드가 무거운 main.tsx 모듈 그래프를 전혀 임포트하지 않고 종료됩니다.

Q2. startMdmRawRead()startKeychainPrefetch()가 다른 임포트 전에 main.tsx 최상위에서 사이드 이펙트로 호출되는 이유는?

  • A) 다른 모듈이 의존하는 프로세스 환경을 초기화하기 때문에
  • B) ESLint가 사이드 이펙트를 먼저 선언하도록 요구하기 때문에
  • C) 약 135ms 임포트 체인과 병렬로 비동기 I/O를 실행하여 필요할 때 결과가 캐시되어 있도록 하기 위해
  • D) 파일 시스템 작업 전에 인증이 이루어지도록 보장하기 위해
MDM 정책 읽기(plutil 경유)와 키체인 읽기는 20~65ms가 소요됩니다. 모듈 평가 중에 실행하면 임포트 체인과 동시에 실행되어 init()이 필요로 할 때 이미 해결되어 있습니다. 순차 읽기는 크리티컬 패스에 해당 지연 시간을 추가할 것입니다.

Q3. setup.ts에서 setCwd(cwd)captureHooksConfigSnapshot()보다 먼저 호출되어야 하는 이유는?

  • A) 훅 스냅샷이 cwd 기준으로 .claude/settings.json을 읽기 때문에, setCwd() 전에 읽으면 잘못된 디렉토리의 훅을 로드하게 됨
  • B) setCwd()가 이후 모든 fs 호출에서 사용하는 Node.js 프로세스 작업 디렉토리를 초기화하기 때문
  • C) 세션 ID가 cwd 설정에 의존하기 때문
  • D) 상관없음 — 주석은 단순 스타일 관례일 뿐
소스 주석이 명시합니다: "setCwd() 이후에 호출해야 올바른 디렉토리에서 훅이 로드됩니다." captureHooksConfigSnapshot()은 프로젝트의 설정 파일을 읽어 구성된 훅의 스냅샷을 찍습니다 — cwd가 의도한 프로젝트 루트가 아닌 셸의 작업 디렉토리인 경우 잘못된 훅이 로드됩니다.

Q4. "bare 모드"(--bare / CLAUDE_CODE_SIMPLE)는 부트 중 무엇을 건너뛰나요?

  • A) 인증 및 API 키 확인
  • B) Commander 인수 파싱 및 마이그레이션
  • C) Node.js 버전 확인 및 권한 안전 게이트
  • D) UDS 메시징 서버, 팀메이트 스냅샷, 세션 메모리, 플러그인 훅, 어트리뷰션 훅, 모든 지연 프리페치
Bare 모드는 스크립트/SDK 호출에 해당하지 않는 불필요한 시작 작업을 모두 건너뜁니다: UDS 소켓, 팀메이트 스냅샷, 세션 메모리, 플러그인 프리페치, 어트리뷰션 훅, 레포 분류, 지연 프리페치 없음. 인증 확인, 마이그레이션, 분석 비컨은 여전히 실행됩니다.

Q5. bootstrap/state.tsafkModeHeaderLatchedfastModeHeaderLatched 같은 필드가 존재하는 이유는?

  • A) 사용자가 실험 기능의 이용 약관에 동의했는지 추적하기 위해
  • B) 모드가 활성화된 후 Anthropic API 요청 헤더를 안정적으로 유지하여 세션 중간 토글 시 서버 측 프롬프트 캐시 무효화를 방지하기 위해
  • C) 빠른 모드나 AFK 모드가 활성화된 후 사용자가 비활성화하지 못하도록 하기 위해
  • D) 매 API 호출마다 설정을 다시 읽지 않아 메모리 사용량을 줄이기 위해
state.ts의 주석에 이것들이 '스티키-온 래치'임이 설명되어 있습니다. AFK 모드, 빠른 모드, 또는 캐시 편집 모드가 처음 활성화되면 해당 API 헤더가 나머지 세션 동안 유지됩니다. 헤더가 GrowthBook/설정 변경마다 토글되면 서버의 캐시된 프롬프트 접두사가 무효화되어 비용이 큰 캐시 미스가 발생합니다(Anthropic 측에서 약 50~70K 토큰 재처리).
0 / 5