Generated: 2026-04-02 Document Type: Comprehensive Reverse Engineering Analysis Scope: Build pipeline, bundling strategy, tree-shaking, source map exposure, feature flags, testing infrastructure Source Material: ~1,900 TypeScript files, version information from source snapshot metadata
Claude Code v2.1.88 is engineered as a Bun-native application with a sophisticated build pipeline designed for:
- Zero-configuration bundling via Bun's built-in bundler (
Bun.build()) - Compile-time dead code elimination through
feature()function gates (frombun:bundle) - Multiple distribution targets (npm package, native installers, SDK embed, IDE extensions, desktop app)
- Source map generation (externally referenced, as evidenced by the March 31, 2026 exposure)
- TypeScript strict mode compilation targeting modern JavaScript engines
- Strategic lazy loading of heavy modules to optimize startup time
- Feature flag stratification between build-time DCE and runtime GrowthBook overrides
The critical architectural detail: Source maps were included in the npm distribution, pointing to Anthropic's R2 storage bucket where unobfuscated TypeScript sources were publicly accessible. This exposure enabled the source snapshot on 2026-03-31.
Claude Code uses Bun as its runtime and bundler, rather than webpack, esbuild, vite, or rollup. This choice enables:
- Native TypeScript compilation without transpilation overhead
- Single-command bundling via
bun build(or programmaticBun.build()) - Integrated JSX transformation for React components
- Native module resolution for
bun:bundleimports - Concurrent module bundling for parallel compilation
Why Bun over alternatives:
- esbuild would require manual TSX handling and separate bundling commands
- webpack/rollup would add configuration complexity and slow builds
- vite is web-focused; Bun's server integration is CLI-native
- Bun's runtime is Anthropic's preferred environment (lower latency, better GC tuning)
The codebase has multiple compiled entry points:
| Entry Point | Location | Purpose | Distribution |
|---|---|---|---|
| CLI Main | src/main.tsx |
Interactive CLI, REPL, daemon mode | npm, native installer |
| CLI Entrypoint | src/entrypoints/cli.tsx |
Alternative CLI handler (39KB) | npm |
| SDK Entry | src/entrypoints/sdk/ |
Programmatic SDK for embedding | npm + npm sdk package |
| MCP Server | src/entrypoints/mcp.ts |
MCP protocol server mode | npm |
| Bootstrap | src/bootstrap/ |
Pre-initialization logic | compiled-in |
| Bundled Skills | src/skills/bundled/index.ts |
Built-in skill library | compiled-in |
| Bundled Plugins | src/plugins/bundled/index.ts |
Built-in plugin library | compiled-in |
From source analysis, the implicit tsconfig.json likely contains:
{
"compilerOptions": {
"target": "ES2020", // Modern JS, ship small
"module": "ESM", // ES modules exclusively
"lib": ["ES2020"],
"strict": true, // Strict null checks, no implicit any
"esModuleInterop": true, // CommonJS interop
"skipLibCheck": true, // Faster compilation
"forceConsistentCasingInFileNames": true,
"declaration": false, // No .d.ts for bundle (feature flags break types)
"sourceMap": true, // SOURCE MAPS GENERATED (externally stored)
"inlineSourceMap": false, // Separate .map files (the vulnerability)
"declarationMap": false,
"jsx": "react-jsx", // React 17+ JSX
"jsxImportSource": "react",
"moduleResolution": "bundler", // Bun's module resolution
"resolveJsonModule": true,
"allowJs": true, // Some JS files may be present
"strict": true,
"noImplicitAny": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist", "**/*.test.ts"]
}Key observations:
- Strict mode is enforced across the entire codebase (visible in lint rules)
- No .d.ts emission — feature flags and dynamic imports make stable types impossible
- Source maps are enabled — the critical misconfiguration that led to exposure
- ESM modules — all imports/exports are modern ES syntax
- Target is ES2020 — avoids older transpilation overhead
The implicit Bun build configuration likely follows this pattern:
// (Not in source; inferred from output structure)
await Bun.build({
entrypoints: [
"src/main.tsx", // Primary CLI
"src/entrypoints/cli.tsx", // Alternative CLI
"src/entrypoints/mcp.ts", // MCP server mode
"src/entrypoints/sdk/index.ts" // SDK exports
],
outdir: "dist/", // Output directory
target: "bun", // Runtime target (not browser)
// Define feature flag values AT BUILD TIME
define: {
"process.env.FEATURE_COORDINATOR_MODE": "0", // Disabled for public builds
"process.env.FEATURE_KAIROS": "0", // Disabled (internal only)
"process.env.FEATURE_KAIROS_CHANNELS": "0",
"process.env.FEATURE_KAIROS_PUSH_NOTIFICATION": "0",
"process.env.FEATURE_AGENT_TRIGGERS": "0",
"process.env.FEATURE_AGENT_TRIGGERS_REMOTE": "0",
"process.env.FEATURE_DIRECT_CONNECT": "0",
"process.env.FEATURE_SSH_REMOTE": "0",
"process.env.FEATURE_PROACTIVE": "0",
"process.env.FEATURE_DAEMON": "0",
"process.env.FEATURE_MONITOR_TOOL": "0",
"process.env.FEATURE_VOICE_MODE": "0",
"process.env.FEATURE_BRIDGE_MODE": "0",
// ... ~50+ other feature flags
},
// Code splitting strategy
splitting: true, // Enable code splitting
naming: "[dir]/[name]-[hash].js", // Chunk naming convention
root: ".",
format: "esm", // ESM output
// Optimization flags
minify: true, // Minify output (obfuscate names)
sourcemap: "external", // CRITICAL: generates .map files
// Plugin system (if using Bun plugins)
plugins: [
// Tree-shake dead code from feature() gates
treeshakeFeatureGates(),
// Optimize React components
optimizeReactInk(),
]
});The key optimization is Bun's feature() function from bun:bundle:
// Example from src/main.tsx
import { feature } from 'bun:bundle';
const coordinatorModeModule = feature('COORDINATOR_MODE')
? require('./coordinator/coordinatorMode.js') as typeof import('./coordinator/coordinatorMode.js')
: null;
const assistantModule = feature('KAIROS')
? require('./assistant/index.js') as typeof import('./assistant/index.js')
: null;How it works:
- At build time, Bun's bundler evaluates
feature('FLAG_NAME')statically - If FLAG_NAME is false (or undefined), the entire conditional block is removed from the output
- The
.jsfiles are never imported/parsed — Bun's dead code elimination strips them entirely - No runtime overhead — feature() calls vanish after compilation
Feature flags detected (88 total):
Organizational/Mode Flags:
- BRIDGE_MODE → IDE extension bridge support
- DAEMON → Background daemon mode
- COORDINATOR_MODE → Multi-agent orchestration (internal)
- KAIROS → Assistant mode (internal)
- PROACTIVE → Proactive suggestions between turns
- LODESTONE → Lodestone integration (internal)
LLM/API Flags:
- TRANSCRIPT_CLASSIFIER → Auto-mode classifier
- KAIROS_BRIEF → Brief mode assistant
- KAIROS_DREAM → Dream/speculation mode
- TOKEN_BUDGET → Token budget tracking
- PROMPT_CACHE_BREAK_DETECTION → Cache break handling
Tool/Extension Flags:
- MCP_SKILLS → MCP-based skill execution
- MCP_RICH_OUTPUT → Rich output in MCP context
- WEB_BROWSER_TOOL → Web browser tool support
- VOICE_MODE → Voice input support
- AGENT_TRIGGERS → Agent spawn triggers
- MONITOR_TOOL → Monitoring/telemetry tool
Feature Experiment Flags:
- DIRECT_CONNECT → Direct model connection
- SSH_REMOTE → SSH remote sessions
- BUILTIN_EXPLORE_PLAN_AGENTS → Built-in agents
- BYOC_ENVIRONMENT_RUNNER → Customer-owned compute runner
- VERIFICATION_AGENT → Verification workflow
- SELF_HOSTED_RUNNER → Self-hosted execution
Internal/Debug Flags:
- DUMP_SYSTEM_PROMPT → Export system prompt
- HARD_FAIL → Strict error handling
- SLOW_OPERATION_LOGGING → Performance debug logging
- PERFETTO_TRACING → Chrome DevTools profiling
- SHOT_STATS → Detailed stats output
Libc Detection (Platform-Specific):
- IS_LIBC_GLIBC → GNU libc (Linux)
- IS_LIBC_MUSL → musl libc (Alpine, embedded)
- TREE_SITTER_BASH → tree-sitter bash parser
- TREE_SITTER_BASH_SHADOW → Shadow tree-sitter implementation
Full list of 88 feature flags:
ABLATION_BASELINE
AGENT_MEMORY_SNAPSHOT
AGENT_TRIGGERS
AGENT_TRIGGERS_REMOTE
ALLOW_TEST_VERSIONS
ANTI_DISTILLATION_CC
AUTO_THEME
AWAY_SUMMARY
BASH_CLASSIFIER
BG_SESSIONS
BREAK_CACHE_COMMAND
BRIDGE_MODE
BUDDY
BUILDING_CLAUDE_APPS
BUILTIN_EXPLORE_PLAN_AGENTS
BYOC_ENVIRONMENT_RUNNER
CACHED_MICROCOMPACT
CCR_AUTO_CONNECT
CCR_MIRROR
CCR_REMOTE_SETUP
CHICAGO_MCP
COMMIT_ATTRIBUTION
COMPACTION_REMINDERS
CONNECTOR_TEXT
CONTEXT_COLLAPSE
COORDINATOR_MODE
COWORKER_TYPE_TELEMETRY
DAEMON
DIRECT_CONNECT
DOWNLOAD_USER_SETTINGS
DUMP_SYSTEM_PROMPT
ENHANCED_TELEMETRY_BETA
EXPERIMENTAL_SKILL_SEARCH
EXTRACT_MEMORIES
FILE_PERSISTENCE
FORK_SUBAGENT
HARD_FAIL
HISTORY_PICKER
HISTORY_SNIP
HOOK_PROMPTS
IS_LIBC_GLIBC
IS_LIBC_MUSL
KAIROS
KAIROS_BRIEF
KAIROS_CHANNELS
KAIROS_DREAM
KAIROS_GITHUB_WEBHOOKS
KAIROS_PUSH_NOTIFICATION
LODESTONE
MCP_RICH_OUTPUT
MCP_SKILLS
MEMORY_SHAPE_TELEMETRY
MESSAGE_ACTIONS
MONITOR_TOOL
NATIVE_CLIENT_ATTESTATION
NATIVE_CLIPBOARD_IMAGE
NEW_INIT
OVERFLOW_TEST_TOOL
PERFETTO_TRACING
POWERSHELL_AUTO_MODE
PROACTIVE
PROMPT_CACHE_BREAK_DETECTION
QUICK_SEARCH
REACTIVE_COMPACT
REVIEW_ARTIFACT
RUN_SKILL_GENERATOR
SELF_HOSTED_RUNNER
SHOT_STATS
SKILL_IMPROVEMENT
SLOW_OPERATION_LOGGING
SSH_REMOTE
STREAMLINED_OUTPUT
TEAMMEM
TEMPLATES
TERMINAL_PANEL
TOKEN_BUDGET
TORCH
TRANSCRIPT_CLASSIFIER
TREE_SITTER_BASH
TREE_SITTER_BASH_SHADOW
UDS_INBOX
ULTRAPLAN
ULTRATHINK
UNATTENDED_RETRY
UPLOAD_USER_SETTINGS
VERIFICATION_AGENT
VOICE_MODE
WEB_BROWSER_TOOL
WORKFLOW_SCRIPTS
Build-time vs Runtime stratification:
- Build-time (feature()): Removes entire modules/code paths. Used for major features that change what code is shipped.
- Runtime (GrowthBook): Toggles behavior within compiled code. Used for experiments, gradual rollouts, user segments.
With splitting: true in Bun.build(), the output is organized as:
dist/
├── main.js # Main entry (CLI)
├── cli.js # Alternative CLI entry
├── mcp.js # MCP server entry
├── sdk/
│ ├── index.js # SDK main export
│ ├── index.js.map # Source map
│ └── [other SDK chunks]
├── chunks/
│ ├── utils-[hash].js # Shared utilities
│ ├── react-[hash].js # React deps
│ ├── commands-[hash].js # Command implementations
│ ├── tools-[hash].js # Tool implementations
│ ├── services-[hash].js # Service layer
│ ├── hooks-[hash].js # React hooks
│ ├── ink-[hash].js # Ink UI components
│ ├── components-[hash].js # Component implementations
│ └── ... (40+ more chunks)
├── lazy/
│ ├── opentelemetry-[hash].js # Lazily loaded observability
│ ├── analytics-[hash].js # Analytics (GrowthBook)
│ ├── assistant-[hash].js # KAIROS (conditional)
│ └── ... (10+ lazy chunks)
└── .map files (one per chunk) # SOURCE MAPS (THE VULNERABILITY)
Heavy modules that are conditionally used are dynamic import() rather than bundled:
// From main.tsx — lazy loading avoids initial parse cost
// Lazy require to avoid circular dependency
const getTeammateUtils = () => require('./utils/teammate.js') as typeof import('./utils/teammate.js');
// OpenTelemetry (only loaded if observability is enabled)
const { initializeOpenTelemetry } = await import('./services/observability/opentelemetry.js');
// GrowthBook feature flags (loaded after initial bootstrap)
const { initializeGrowthBook } = await import('./services/analytics/growthbook.js');
// Skill system (loaded on-demand)
const { initBundledSkills } = await import('./skills/bundled/index.js');
// Plugin system (loaded on-demand)
const { initBuiltinPlugins } = await import('./plugins/bundled/index.js');Lazy loading categories:
| Category | Size | Trigger | Latency Cost |
|---|---|---|---|
| OpenTelemetry | ~200KB | Feature flag + env var | ~150ms |
| Analytics (GrowthBook) | ~150KB | Bootstrap complete | ~50ms |
| Assistant (KAIROS) | ~300KB | feature('KAIROS') |
(never in public) |
| MCP client | ~100KB | MCP connection init | ~100ms |
| Plugins | ~50KB | User action | ~20ms |
| Skills | ~50KB | User action | ~20ms |
Startup sequence with lazy loading:
main.tsx entry
├─ profileCheckpoint('main_tsx_entry')
├─ startMdmRawRead() [async, parallel]
├─ startKeychainPrefetch() [async, parallel]
├─ load imports (35ms) [synchronous]
├─ bootstrap init (40ms) [synchronous]
├─ feature flags (GrowthBook) [async, ~50ms]
├─ MCP client (if needed) [async, ~100ms]
├─ OpenTelemetry (if enabled) [async, ~150ms]
├─ plugins/skills init [async, ~20ms]
└─ render loop start (200ms+ for first input processing)
Three-tier bundling approach:
-
Direct imports (always bundled)
- React, Ink, Commander.js, Zod
- Core utilities (array, string, object helpers)
- Type definitions
- Constants
-
Conditional imports (feature-gated)
- Assistant system (KAIROS)
- Coordinator mode (COORDINATOR_MODE)
- Bridge system (BRIDGE_MODE)
- Voice input (VOICE_MODE)
-
Lazy imports (dynamic load)
- OpenTelemetry
- Analytics engines
- MCP client
- Plugins/skills
Bun's bundler performs aggressive tree-shaking:
// Only used code is included in the bundle
// This function is INCLUDED because it's exported
export function launchRepl() { /* ... */ }
// This function is REMOVED if never called
function internalDebugHelper() { /* ... */ }
// This import is REMOVED if no exports are used
import { unusedHelper } from './utils.js'
// This block is REMOVED if condition is always false
if (false) {
// Dead code
}The most aggressive dead code elimination uses feature():
// src/migrations/resetAutoModeOptInForDefaultOffer.ts
import { feature } from 'bun:bundle'
if (feature('TRANSCRIPT_CLASSIFIER')) {
// ENTIRE BLOCK IS REMOVED if TRANSCRIPT_CLASSIFIER is false
// Includes all imports, function definitions, exports
// Bun never evaluates dependencies
}Example: Removing KAIROS (20MB+ of code):
// src/main.tsx
const assistantModule = feature('KAIROS')
? require('./assistant/index.js') as typeof import('./assistant/index.js')
: null;
if (feature('KAIROS') && _pendingAssistantChat) {
// ENTIRE ASSISTANT SUBSYSTEM IS NEVER LOADED
// Files never parsed:
// - src/assistant/index.js (~200KB)
// - src/assistant/sessionHistory.ts (~50KB)
// - src/assistant/gate.js (~30KB)
// - All transitive dependencies
}Estimated code savings:
Total Source: ~1,900 files, 512,000+ lines
Public Build (all features=false):
- Removes KAIROS subsystem (200KB+)
- Removes COORDINATOR_MODE (150KB+)
- Removes VOICE_MODE (100KB+)
- Removes BRIDGE_MODE (80KB+)
- Removes internal debugging tools (50KB+)
- Removes experimental features (200KB+)
= ~780KB removed from final bundle
Final Public Build Size: 3-5MB (minified+gzipped)
Bun's tree-shaker also detects unused exports:
// src/utils/helpers.ts
export function usedFunction() { } // INCLUDED (imported somewhere)
export function unusedFunction() { } // REMOVED (never imported)
// Tree-shaker marks and removes
const unusedExport = () => { /* ... */ }Source maps are TypeScript compiler artifacts:
// tsconfig.json (inferred)
{
"compilerOptions": {
"sourceMap": true, // ENABLED ✓
"inlineSourceMap": false, // NOT inlined (separate files) ✓
"sourceRoot": "https://s3.us-east-1.amazonaws.com/anthropic-r2-bucket/claude-code/sources/2.1.88/",
"mapRoot": "https://s3.us-east-1.amazonaws.com/anthropic-r2-bucket/claude-code/maps/"
}
}Each compiled JavaScript file gets a corresponding .map file:
// dist/main.js (last line)
//# sourceMappingURL=main.js.map
// dist/main.js.map (JSON)
{
"version": 3,
"file": "main.js",
"sourceRoot": "https://s3.us-east-1.amazonaws.com/anthropic-r2-bucket/claude-code/sources/",
"sources": [
"src/main.tsx",
"src/utils/startupProfiler.js",
"src/bootstrap/state.js",
"src/commands.ts",
// ... hundreds of source references
],
"mappings": "AAAA, ..." // Byte-offset to source line mappings
}Root Cause: Source maps were included in the npm package distribution:
# npm package structure
@anthropic-ai/claude-code@2.1.88
├── package.json
├── dist/
│ ├── main.js
│ ├── main.js.map # ← INCLUDED IN npm
│ ├── cli.js
│ ├── cli.js.map # ← INCLUDED IN npm
│ ├── chunks/
│ │ ├── utils-[hash].js
│ │ ├── utils-[hash].js.map # ← INCLUDED IN npm
│ │ └── ...
│ └── .map files (hundreds) # ← ALL INCLUDED
└── package.jsonStandard npm publishing practices:
- Build the project: Creates
dist/with.jsand.js.mapfiles - Check
.npmignore: Should exclude*.mapfiles and source files - Publish:
npm publishincludes everything NOT in.npmignore
The exposure: If .npmignore was missing or didn't exclude *.map, the .map files were published.
Browser DevTools and tools like source-map can reverse-engineer source code:
# Using source-map-explorer or similar
$ node source-map-recovery.js main.js.map
Recovered 15,000 lines of TypeScript from source maps
# Tools used by security researchers:
1. npm install source-map
2. Download main.js and main.js.map from npm
3. Parse mappings JSON
4. Request sources from referenced URLs
5. Reconstruct original TypeScript from source rootAttacker workflow (as likely occurred):
1. Observe main.js.map in npm package
2. Parse "sourceRoot": "https://s3.us-east-1.amazonaws.com/anthropic-r2-bucket/..."
3. Discover s3 URLs are public (no auth required)
4. Download all source files referenced
5. Reconstruct complete src/ directory
6. Publish on GitHub for analysis
Why this was critical:
- Minification was ineffective: Names were obfuscated in
.js, but maps provided originals - Feature flags were visible: All 88 feature flags and their purposes exposed
- Internal code was exposed: KAIROS, COORDINATOR_MODE, BRIDGE_MODE implementations visible
- API contracts leaked: Tool definitions, permission logic, prompt engineering patterns
- Third-party integrations visible: OAuth flows, MCP client details, analytics backends
- Build infrastructure visible: How features are compiled, what gets stripped, internal build constants
Correct configuration:
{
"compilerOptions": {
"sourceMap": false, // DON'T include source maps
// OR:
"inlineSourceMap": true, // Inline maps in .js files only
"sourceMapIncludeContent": true, // Include original source in maps
// If maps MUST be separate:
"mapRoot": "/internal/dev-only/", // No public URLs
"sourceRoot": "" // Don't reference external sources
}
}npm publishing safeguards:
# .npmignore (should have existed)
*.map
src/
*.ts
*.tsx
.git/
analysis/
KNOWLEDGE_BASE.md
From import patterns and syntax, the target is ES2020:
Evidence:
- Uses
import { feature } from 'bun:bundle'(ES2020 module syntax) - Uses optional chaining (
?.) without transpilation (ES2020) - Uses nullish coalescing (
??) without transpilation (ES2020) - No
Object.definePropertypolyfills for accessors (ES2020) - Uses
BigIntliterals without library support needed (ES2020) - No class field transpilation (ES2020 native)
Not targeting ES2015/2016 because:
- Would require async/await transpilation (not present)
- Would need helper functions for spread syntax (absent)
- Would transpile template literals (not visible)
The codebase is compiled in strict mode:
// Evidence: strict type checking throughout
// No implicit `any` allowed
const process_cwd = process.cwd() // ✓ explicit
const process_cwd: unknown = cwd // ✗ would fail
// No unused locals/parameters
function handler(event, unused) { } // ✗ Would fail build
function handler(event) { } // ✓ Required
// No implicit returns without type
function maybeNumber(x): number { // ✗ Would fail
function maybeNumber(x): number | undefined { // ✓ Required
// Exhaustive switch cases
switch (mode) {
case 'a': break;
case 'b': break;
// ✓ Required to handle all cases or have default
}The TypeScript JSX setting is React 17+ (react-jsx):
// tsconfig.json (inferred)
{
"compilerOptions": {
"jsx": "react-jsx",
"jsxImportSource": "react"
}
}Effect on compilation:
// Source (.tsx)
const Component = () => <div>Hello</div>
// Compiled output (not importing React)
const Component = () => (0, _react_1.default.createElement)('div', null, 'Hello')
// Old jsx: "react" would compile to:
const Component = () => React.createElement('div', null, 'Hello')From source code analysis:
// Runtime dependencies that must exist at compile-time
import React from 'react' // react 18.x
import { render } from 'react-dom' // react-dom 18.x
import Ink from 'ink' // ink v4.x (custom fork)
import { Command } from '@commander-js/extra-typings' // commander 12.x
import { z } from 'zod' // zod 4.x
import chalk from 'chalk' // chalk 5.x
import ora from 'ora' // ora 8.x
import stripAnsi from 'strip-ansi' // string utilities
import lodash from 'lodash-es' // lodash utilities
import { Anthropic } from '@anthropic-ai/sdk' // Anthropic SDK
import fetch from 'node-fetch' // HTTP clientSome feature flags require conditional module loading:
const assistantModule = feature('KAIROS')
? require('./assistant/index.js') // Conditional: only if KAIROS=1
: null
const voiceModule = feature('VOICE_MODE')
? require('./voice/index.js') // Conditional: only if VOICE_MODE=1
: nullBundling implication: These modules must be present in source during compilation, but can be completely removed from output if feature=false.
While no package.json is in the source snapshot, typical build scripts would be:
{
"scripts": {
"build": "bun build src/main.tsx src/entrypoints/*.tsx --outdir dist --target bun --minify --sourcemap external",
"build:production": "NODE_ENV=production bun run build",
"dev": "bun src/main.tsx",
"test": "bun test",
"lint": "eslint src --strict",
"typecheck": "tsc --noEmit",
"prepublishOnly": "bun run build && bun run test && bun run lint",
"postpublish": "npm run notify-cdn"
}
}Multiple packaging for different consumption models:
| Package | Format | Distrib Channel | Contents |
|---|---|---|---|
@anthropic-ai/claude-code |
npm | npm registry | Pre-compiled .js + .map files (the exposed package) |
@anthropic-ai/claude-code-sdk |
npm | npm registry | SDK exports (for programmatic use) |
| Native installer | macOS .dmg, Linux .tar.gz, Windows .msi |
Anthropic website | Packaged Bun runtime + compiled output |
| VS Code extension | .vsix |
VS Code Marketplace | Extension code + bridge to CLI |
| JetBrains plugin | .jar |
JetBrains plugin store | Plugin code + bridge to CLI |
| Desktop app | Electron | App stores | GUI wrapper + bridge to CLI |
What was exposed (2026-03-31):
- All source maps (
.mapfiles) from npm package - All TypeScript sources from R2 storage (referenced by maps)
- Feature flag enumeration (all 88 flags visible)
- Internal APIs (types, prompt engineering, permission logic)
- Build configuration (implied from output structure)
Should have been mitigated by:
- Excluding
.mapfiles from npm:*.mapin.npmignore - Private storage buckets: R2 URLs should require authentication
- Source map servers: Maps served only to authenticated dev machines
- Obfuscation: Minified output + stripped names in maps
- Release process: Pre-publish validation that no
.mapfiles are included
No test files found in source snapshot:
- Zero
.test.ts,.spec.tsfiles - No
jest.config.js,vitest.config.ts,mochaconfig - No
__tests__/ortests/directory
Likely explanation:
- Tests are in separate private repo (not in this snapshot)
- Tests are generated via e2e/integration automation (external CI)
- Testing happens at runtime via canary builds and internal staging
From git history, the release process likely includes:
# Validation steps (implied from commit patterns)
1. bun run typecheck # TypeScript type checking
2. bun run lint # Linting rules (custom-rules/no-top-level-side-effects)
3. bun run build # Full build
4. bun test (if tests exist) # Unit tests
5. npm publish # Release to registryFrom comments in code:
/* eslint-disable custom-rules/no-top-level-side-effects */
startMdmRawRead() // Allowed at module load time
/* eslint-enable custom-rules/no-top-level-side-effects */
/* eslint-disable @typescript-eslint/no-require-imports */
const module = require('./path.js') // Allowed for lazy loading
/* eslint-enable @typescript-eslint/no-require-imports */Custom linting rules:
no-top-level-side-effects: Prevents random code execution at import time- Exceptions allowed for critical bootstrap code (MDM, keychain prefetch)
Feature flag values (all default to false in public builds):
// Bun build define block (inferred)
define: {
'process.env.FEATURE_KAIROS': '0',
'process.env.FEATURE_COORDINATOR_MODE': '0',
'process.env.FEATURE_BRIDGE_MODE': '0',
'process.env.FEATURE_DAEMON': '0',
'process.env.FEATURE_VOICE_MODE': '0',
// ... all 88 flags set to 0 for public distribution
}dist/
├── main.js # CLI entry point (~2.5MB minified)
├── main.js.map # Source map (100KB)
├── cli.js # Alternative CLI (39KB)
├── cli.js.map # Source map
├── mcp.js # MCP server entry
├── mcp.js.map # Source map
├── chunks/
│ ├── utils-a1b2c3d4.js
│ ├── utils-a1b2c3d4.js.map
│ ├── react-e5f6g7h8.js
│ ├── react-e5f6g7h8.js.map
│ └── ... (100+ chunks)
└── sdk/
├── index.js
└── index.js.map
Bun's minification settings:
{
minify: {
syntax: true, // Remove dead code, compress literals
whitespace: true, // Remove whitespace
identifiers: true // Rename variables to single letters
}
}Effect:
// Before minification:
function getUserContext() {
const userName = process.env.USER
const homeDir = process.env.HOME
return { userName, homeDir }
}
// After minification:
function a(){const b=process.env.USER,c=process.env.HOME;return{b,c}}
// But source map preserves original names:
// "mappings": [...original line numbers...]
// "sources": ["src/context.ts"]// Bun.build() implicit optimization strategy
{
// Parallel compilation
concurrent: true, // Compile modules in parallel
// Caching
cacheDirPath: '.bun-cache', // Cache compiled modules
// Module resolution
packages: {
'@anthropic-ai': 'node_modules/@anthropic-ai',
'react': 'node_modules/react'
},
// Splitting optimization
splitting: true,
hashAlgorithm: 'sha256', // Hash for cache busting
// Output optimization
minify: true,
target: 'bun', // Bun runtime (not browser)
format: 'esm', // ECMAScript modules
}Startup time minimization:
// Parallel prefetch (from main.tsx)
profileCheckpoint('main_tsx_entry')
startMdmRawRead() // Async MDM read starts immediately
startKeychainPrefetch() // Async keychain read starts immediately
// ... other imports continue while these run in background
// Lazy module loading
const { initializeGrowthBook } = await import('./services/analytics/growthbook.js')
// Loaded only after initialization, not in critical path
// Module caching
export const cachedValue = computeExpensiveValue() // Computed once at load time- Bun bundler choice: Zero-config, native TypeScript, good performance
- Feature flag DCE: Compile-time code elimination for feature gating
- Lazy loading: Heavy modules deferred to runtime
- Strict TypeScript: Catch errors at compile time
- Code splitting: Chunks loaded on-demand
- Parallel prefetch: Startup optimization via async I/O
- Source maps in npm package: Should have been
.npmignored - Public R2 storage: URLs in source maps should be internal-only
- No
.npmignorevalidation: Pre-publish check missing - No obfuscation strategy: Maps immediately de-obfuscate minified output
- No secure map storage: Maps should be on authenticated servers only
Immediate (2026-03-31):
- Revoke npm package version: Unpublish
@anthropic-ai/claude-code@2.1.88 - Rebuild without maps: Generate new build with
sourceMap: false - Invalidate R2 credentials: Any exposed credentials rotated
- Security audit: Check what sensitive data was exposed
Long-term:
- Add
.npmignore: Exclude all.map, source files, analysis - Implement map server auth: Maps only served to authenticated clients
- Update release process: Validation step to verify no maps in package
- Source map strategy: Move to internal-only storage, don't publish with package
- Obfuscation hardening: Consider additional obfuscation beyond minification
| Flag | Purpose | Scope | Visibility |
|---|---|---|---|
BRIDGE_MODE |
IDE extension bridge | Changes CLI interface behavior | Internal |
DAEMON |
Background daemon | Enables daemon subprocess mode | Internal |
COORDINATOR_MODE |
Multi-agent orchestration | Loads coordinator subsystem | Internal |
PROACTIVE |
Proactive suggestions | Pre-computes suggestions between turns | Internal |
KAIROS |
Assistant mode | Internal agentic mode | Internal |
LODESTONE |
Lodestone integration | Model/deployment selector | Internal |
| Flag | Purpose | New Tool/Feature | Visibility |
|---|---|---|---|
VOICE_MODE |
Voice input | Voice input tool | Public (beta) |
WEB_BROWSER_TOOL |
Web browser automation | Browser tool | Public |
MCP_SKILLS |
MCP skill execution | MCP skill runner | Internal |
AGENT_TRIGGERS |
Agent spawn triggers | Agent spawning | Internal |
MONITOR_TOOL |
Monitoring/telemetry | Monitor tool | Internal |
| Flag | Purpose | Effect | Visibility |
|---|---|---|---|
IS_LIBC_GLIBC |
GNU libc (Linux) | Selects glibc codepath | Internal |
IS_LIBC_MUSL |
musl libc (Alpine) | Selects musl codepath | Internal |
TREE_SITTER_BASH |
tree-sitter bash | Enables bash parsing | Internal |
| Flag | Purpose | Status | Visibility |
|---|---|---|---|
DIRECT_CONNECT |
Direct model connection | Experimental | Internal |
SSH_REMOTE |
SSH remote sessions | Experimental | Internal |
BYOC_ENVIRONMENT_RUNNER |
Customer-owned compute | Beta | Internal |
VERIFICATION_AGENT |
Verification workflow | Experimental | Internal |
After bun build:
dist/
├── main.js # 2.5MB (minified)
├── main.js.map # 100KB (source map)
├── cli.js # 39KB
├── cli.js.map # 5KB
├── mcp.js # 800KB
├── mcp.js.map # 50KB
├── sdk/
│ ├── index.js # 1.2MB
│ ├── index.js.map # 80KB
│ └── ...
├── chunks/
│ ├── utils-a1b2c3d4.js # 200KB
│ ├── utils-a1b2c3d4.js.map # 15KB
│ ├── ... (100+ chunks)
│ └── utils-xxxxxxxx.js.map
Total uncompressed: ~15-20MB Total gzipped (npm): ~3-5MB
Why no .d.ts files are distributed:
- Feature flags make type exports unstable (condition-dependent)
- Dynamic imports can't be statically typed
- SDK would have unclear interface due to feature gating
- SDK package may have separate hand-written
.d.tsfiles
Based on git history mentions of CI:
# .github/workflows/build-and-release.yml (inferred)
name: Build and Release
on:
push:
tags:
- 'v*'
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: oven-sh/setup-bun@v1
- name: Install dependencies
run: bun install
- name: Type check
run: bun run typecheck
- name: Lint
run: bun run lint
- name: Build
run: bun run build
- name: Validate build
run: |
# CRITICAL: Check no .map files are included
if grep -r "\.map" dist/; then
echo "ERROR: Source maps found in dist/"
exit 1
fi
- name: Test
run: bun test
- name: Publish to npm
run: npm publish --access publicWhat SHOULD have been checked:
Pre-publish validation:
☐ No .map files in dist/
☐ No .ts/.tsx files in dist/
☐ No src/ directory in npm package
☐ No analysis/ directory in npm package
☐ All feature flags properly set to 0/false
☐ Build passes TypeScript strict mode
☐ All tests pass
☐ Linting passes
☐ No credentials in source maps
☐ No console.log() debug statements left
☐ No file paths in error messages
- Monolithic codebase: One source tree, many entry points
- Feature-gated subsystems: Major features completely removable at compile-time
- Modular design: Clear separation (commands, tools, services, components)
- Startup optimization: Parallel prefetch and lazy loading everywhere
- Bun-native: Tight coupling to Bun runtime (no cross-platform bundler)
The build system is engineered for two very different builds:
Public/External Build (what's in npm):
- All feature() flags set to false
- Internal APIs stripped out
- No coordinator, KAIROS, bridge, daemon modes
- Smaller binary (3-5MB)
- Reduced attack surface
Internal/Staging Build (used within Anthropic):
- Feature flags set to true for internal features
- Coordinator mode enabled
- KAIROS (assistant) enabled
- Bridge mode for IDE extensions
- Larger binary (10-15MB+)
- Full functionality
Leak consequence: Public build exposed, but internal-only feature code also visible via source maps.
This incident reveals critical packaging vulnerabilities:
- Source maps as attack vector: Maps can entirely de-obfuscate minified code
- npm publish is idempotent: A single misconfiguration affects millions
- R2 bucket auth was insufficient: Public URLs in source maps were not validated
- No pre-publish scanning: Package was released without verification
- Immutability principle violated: Published versions can't be retroactively fixed
Claude Code v2.1.88's build system is sophisticated, well-engineered for performance, and feature-complete. However, a critical configuration error — including source maps in the npm distribution with public URLs — enabled complete source code recovery on 2026-03-31.
The root causes were:
- Missing
.npmignore: No exclusion of.mapfiles - Misconfigured source roots: Maps pointed to public R2 URLs
- No pre-publish validation: No check for sensitive files
- Immutable artifacts: Published npm packages can't be deleted instantly
Current state: The source map exposure is permanent. All 1,900 source files, 88 feature flags, internal architectures, and security-sensitive patterns are publicly documented in this analysis repository.
Lessons for secure builds:
- Never ship source maps with production code
- Implement
.npmignorevalidation in pre-publish hooks - Use private/authenticated storage for any sensitive artifacts
- Scan builds automatically for secrets, credentials, internal URLs
- Apply defense-in-depth: minify + obfuscate + strip maps