ClOr

ClOr

백엔드 실무 트러블슈팅과 AI 에이전트 구조 분석을 기록합니다.

Claude Code 해부학 (완결)

51만 줄 소스코드를 19편에 걸쳐 분석한 완결 시리즈

전체 시리즈 보기 →

백엔드 트러블슈팅

실무에서 겪은 장애와 해결 과정 기록

전체 시리즈 보기 →

최신 글

article thumbnail

결론부터 말하면, 클로드 코드(Claude Code) 소스코드 유출의 원인은 빌드 설정 한 줄이었습니다. Bun으로 1,902개 TypeScript 파일을 단일 cli.mjs로 번들링하는 과정에서, --no-sourcemap 플래그를 빼먹은 채 npm에 배포했습니다. 57MB짜리 .map 파일 하나가 원본 소스코드 전체를 복원할 수 있는 열쇠였고, 이 시리즈 전체가 그 실수 덕분에 가능해진 분석입니다.

시리즈: Part 14 — IDE 브릿지 분석 / Claude Code 소스코드 유출 사건 정리


목차

  • Bun인데 왜 esbuild를 쓸까?
  • 빌드 파이프라인은 어떻게 생겼나?
  • 빌드 타임에 뭘 주입하고 뭘 제거할까?
  • 소스맵이 왜 유출 원인이 되었나?
  • tsconfig과 Biome는 어떤 역할을 하나?
  • 이 사례에서 뭘 배울 수 있나?

Bun인데 왜 esbuild를 쓸까?

package.jsonengines 필드에 "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.jsonbaseUrl: "."로 설정되어 있어서 import ... from 'src/foo/bar.js' 형태의 절대 경로 import를 가능하게 합니다. 빌드 스크립트의 srcResolverPlugin이 이 경로를 실제 .ts 파일로 매핑하는 역할이에요.

린터와 포매터는 Biome를 사용합니다. 들여쓰기 탭, 작은따옴표, 세미콜론은 필요할 때만. ESLint + Prettier 조합 대신 Biome 하나로 통합한 것도 Bun 생태계와 잘 어울리는 선택입니다.


이 사례에서 뭘 배울 수 있나?

첫째, 소스맵은 디버깅 도구지 배포 산출물이 아닙니다. .npmignorepackage.jsonfiles 필드로 .map 파일을 명시적으로 제외해야 해요. 둘째, 빌드 스크립트의 기본값이 안전한 방향이어야 합니다. 기본이 'external'인 건 개발 편의용이지 프로덕션 배포에 적합한 설정이 아닙니다.

셋째, 피처 플래그로 내부/외부 빌드를 분리하는 보안 설계가 소스맵 하나로 무너졌습니다. DCE로 코드를 아무리 제거해도, 소스맵에는 제거 전 원본이 남아 있으니까요.

관련 글: Claude Code 프로젝트 구조 분석


profile

ClOr

@ClOr

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!

ClOr · 백엔드 트러블슈팅과 AI 에이전트 구조 분석을 기록합니다.