결론부터 말하면, 클로드 코드(Claude Code) 소스코드 유출의 원인은 빌드 설정 한 줄이었습니다. Bun으로 1,902개 TypeScript 파일을 단일 cli.mjs로 번들링하는 과정에서, --no-sourcemap 플래그를 빼먹은 채 npm에 배포했습니다. 57MB짜리 .map 파일 하나가 원본 소스코드 전체를 복원할 수 있는 열쇠였고, 이 시리즈 전체가 그 실수 덕분에 가능해진 분석입니다.
목차
- Bun인데 왜 esbuild를 쓸까?
- 빌드 파이프라인은 어떻게 생겼나?
- 빌드 타임에 뭘 주입하고 뭘 제거할까?
- 소스맵이 왜 유출 원인이 되었나?
- tsconfig과 Biome는 어떤 역할을 하나?
- 이 사례에서 뭘 배울 수 있나?
Bun인데 왜 esbuild를 쓸까?
package.json의 engines 필드에 "bun": ">=1.1.0"이 명시되어 있습니다. 패키지 매니저부터 빌드 스크립트 실행까지 전부 Bun 기반이에요. TypeScript 파일을 별도 컴파일 없이 bun scripts/build-bundle.ts로 바로 실행할 수 있는 게 Bun의 장점이죠.
그런데 실제 번들링 엔진은 esbuild입니다. 빌드 스크립트 상단이 import * as esbuild from 'esbuild'로 시작해요. Bun은 런타임 + 패키지 매니저 역할이고, 번들링은 esbuild에 맡기는 하이브리드 구조입니다.
빌드 파이프라인은 어떻게 생겼나?
핵심 설정은 build-bundle.ts에 모두 들어 있습니다.
// scripts/build-bundle.ts
const buildOptions: esbuild.BuildOptions = {
entryPoints: [resolve(ROOT, 'src/entrypoints/cli.tsx')],
bundle: true,
platform: 'node',
target: ['node20', 'es2022'],
format: 'esm',
outdir: resolve(ROOT, 'dist'),
outExtension: { '.js': '.mjs' },
splitting: false,
plugins: [srcResolverPlugin],
sourcemap: noSourcemap ? false : 'external',
minify,
treeShaking: true,
}
진입점은 src/entrypoints/cli.tsx 하나입니다. 여기서 1,902개 파일을 전부 순회해서 splitting: false로 코드 스플리팅 없이 단일 cli.mjs를 만듭니다. 출력 상단에는 #!/usr/bin/env node shebang이 붙어서 직접 실행 가능한 CLI 바이너리가 됩니다.

빌드 타임에 뭘 주입하고 뭘 제거할까?
esbuild의 define 옵션으로 상수를 주입하는 부분이 흥미롭습니다.
define: {
'MACRO.VERSION': JSON.stringify(version),
'MACRO.PACKAGE_URL': JSON.stringify('@anthropic-ai/claude-code'),
'process.env.USER_TYPE': '"external"',
'process.env.NODE_ENV': minify ? '"production"' : '"development"',
},
process.env.USER_TYPE이 "external"로 하드코딩되는 게 핵심입니다. 이 값 때문에 @ant/*로 시작하는 Anthropic 내부 패키지를 참조하는 코드 경로가 번들에서 제거돼요. 내부 빌드에서는 이 값이 "ant"로 설정되겠죠.
여기에 더해 bun:bundle의 피처 플래그 시스템도 있습니다. 소스 코드 196곳에서 feature() 함수를 호출하는데, VOICE_MODE, COORDINATOR_MODE 같은 25개 플래그가 빌드 타임에 true/false로 치환되고, false인 분기는 Dead Code Elimination으로 번들에서 사라집니다.
소스맵이 왜 유출 원인이 되었나?
문제의 핵심은 이 한 줄입니다.
sourcemap: noSourcemap ? false : 'external',
--no-sourcemap 플래그 없이 빌드하면 기본값이 'external'이에요. 프로덕션 빌드 명령이 bun scripts/build-bundle.ts --minify인데, 여기에 --no-sourcemap이 빠져 있었습니다.
external 소스맵이란 .mjs 파일 옆에 .mjs.map 파일이 별도 생성된다는 뜻이고, 이 .map 파일 안에는 번들에 포함된 모든 원본 소스 코드가 그대로 들어가 있습니다. 자물쇠는 잠갔는데 열쇠를 문에 꽂아둔 격이에요.
tsconfig과 Biome는 어떤 역할을 하나?
tsconfig.json은 baseUrl: "."로 설정되어 있어서 import ... from 'src/foo/bar.js' 형태의 절대 경로 import를 가능하게 합니다. 빌드 스크립트의 srcResolverPlugin이 이 경로를 실제 .ts 파일로 매핑하는 역할이에요.
린터와 포매터는 Biome를 사용합니다. 들여쓰기 탭, 작은따옴표, 세미콜론은 필요할 때만. ESLint + Prettier 조합 대신 Biome 하나로 통합한 것도 Bun 생태계와 잘 어울리는 선택입니다.
이 사례에서 뭘 배울 수 있나?
첫째, 소스맵은 디버깅 도구지 배포 산출물이 아닙니다. .npmignore나 package.json의 files 필드로 .map 파일을 명시적으로 제외해야 해요. 둘째, 빌드 스크립트의 기본값이 안전한 방향이어야 합니다. 기본이 'external'인 건 개발 편의용이지 프로덕션 배포에 적합한 설정이 아닙니다.
셋째, 피처 플래그로 내부/외부 빌드를 분리하는 보안 설계가 소스맵 하나로 무너졌습니다. DCE로 코드를 아무리 제거해도, 소스맵에는 제거 전 원본이 남아 있으니까요.
관련 글: Claude Code 프로젝트 구조 분석
'AI 코딩 에이전트' 카테고리의 다른 글
| Claude Code 비용 추적 분석: 토큰 하나하나를 관리하는 구조 (해부학 Part 17) (0) | 2026.04.02 |
|---|---|
| Claude Code에 숨겨진 비밀 기능들: 다마고치, 카피바라, 언더커버 모드 (해부학 Part 16) (0) | 2026.04.02 |
| Claude Code IDE 브릿지 분석: VS Code 안에서 동작하는 원리 (해부학 Part 14) (0) | 2026.04.02 |
| Claude Code 터미널 UI 분석: React로 터미널 앱을 만든다고? (해부학 Part 13) (0) | 2026.04.02 |
| Claude Code 메모리 시스템 분석: 어제 대화를 기억하는 법 (해부학 Part 12) (0) | 2026.04.02 |
