← Index
Source: docs/logs/2026-04-02/sourcemap-leak-demo.md (auto-generated by scripts/generate-docs-html.mjs — edit the .md, not this file)

Source Map Leak Demo Project (2026-04-02)

Category: Research

Built a hands-on demo project reproducing the Claude Code npm source map leak incident (2026-03-31) — showing how .map files in production packages expose full original source code, and how to prevent it.


What was done

Techniques & Learnings


How Source Map Deciphering Works

Anatomy of a .map file

A source map is just JSON. Every field plays a role in mapping compiled output back to the original source:

{
  "version":        3,                          // always 3 (current spec)
  "file":           "templates.js",              // compiled output filename
  "sources":        ["../../src/prompts/templates.ts"], // original file path(s)
  "names":          [],                          // original variable/function names
  "mappings":       "AAAA;GAMG;AAEU,QAAA...",    // Base64 VLQ encoded positions
  "sourcesContent": ["full original source..."]  // THE JACKPOT
}

Two independent ways to recover the original code from this structure exist. The Claude Code leak exposed both.

Method 1 (EASY): sourcesContent

The sourcesContent array contains the complete original source files as plain strings. No decoding. No reverse engineering. One line of code:

// That's it. The entire original TypeScript file.
const original = JSON.parse(fs.readFileSync("templates.js.map")).sourcesContent[0];

This is what the Claude Code incident exposed. The .map files shipped in the npm package had every internal TypeScript file — system prompts, agent orchestration, tool routing — embedded verbatim in this field.

Method 2 (HARD): mappings (Base64 VLQ)

Even without sourcesContent, the mappings field encodes a precise position-by-position mapping from every character in the compiled JS back to the original source. It uses Base64 VLQ (Variable-Length Quantity) encoding.

Step 1 — delimiters:

Step 2 — decode Base64 VLQ: each segment (e.g. AAEU) is a sequence of Base64 characters. Each character encodes 6 bits:

Bit Purpose
Bit 6 (highest) Continuation flag — is there another character after this?
Bits 1-5 Data payload — 5 bits of the actual number
Bit 1 (first char only) Sign bit — 0 = positive, 1 = negative

Base64 alphabet: A=0, B=1, ..., Z=25, a=26, ..., z=51, 0=52, ..., 9=61, +=62, /=63.

Step 3 — interpret deltas: each decoded segment produces 4 or 5 numbers, all relative to the previous segment:

Position Meaning
[0] Generated column (in compiled .js)
[1] Source file index (which original file in sources array)
[2] Original line (in original .ts)
[3] Original column (in original .ts)
[4] Names index (optional — into the names array)

By walking every segment in the mappings string, you reconstruct a complete line-by-line, column-by-column correspondence between compiled output and original source. Combined with the names array, an attacker can recover significant structure even without sourcesContent.

Why source maps exist at all

Source maps exist for one reason: debugging. When your production app crashes, the browser shows you something useless:

Error at dist/bundle.min.js:1:24853

Line 1, column 24,853 of a minified file. Impossible to debug. With a source map, the browser's DevTools reads the mappings and translates:

dist/bundle.min.js:1:24853  →  src/auth/login.ts:42:8

Tools that use mappings:

Tool Uses mappings for
Chrome DevTools Show original source in debugger, set breakpoints in .ts files
Error monitoring (Sentry, etc.) Convert minified stack traces to readable ones
Node.js --enable-source-maps Show original file/line in server-side errors

Source maps were designed for development environments. The mistake is shipping them to production where anyone can read them.

Why sourcesContent is the real vulnerability

In an npm package, you typically only get:

node_modules/some-package/
  dist/
    index.js          ← compiled JS
    index.js.map      ← source map
    index.d.ts        ← type declarations

The src/ folder with original .ts files is not shipped. So the sources field pointing to "../../src/prompts/templates.ts" is a dead path. That's exactly why sourcesContent is the critical field:

Field Points to Available in npm package?
sources "../../src/prompts/templates.ts" Nosrc/ not shipped
sourcesContent Full original TypeScript as a string Yes — embedded right in the .map JSON

Without sourcesContent, the .map file is mostly harmless. The mistake was that TypeScript's inlineSources: true in tsconfig.json baked the full source into sourcesContent, and the build pipeline didn't strip the .map files before npm publish.

Two levels of exposure — summary

Level Field What it reveals Effort to exploit
EASY sourcesContent Complete original source code, verbatim Zero — JSON.parse() and read
HARD mappings + names Line/column mapping + variable names Decode Base64 VLQ, reconstruct structure

The Claude Code npm package contained both. Full source recovery was trivial.


TypeScript Patterns Found in the Leaked Code

async function* — The Agent Loop Pattern

The leaked Claude Code source revealed a core pattern: the agent's main loop is an async generator:

export async function* query(
  params: QueryParams,
): AsyncGenerator<
  | StreamEvent
  | RequestStartEvent
  | Message
  | ToolUseSummaryMessage,   // TYield — values that come out
  Terminal                    // TReturn — final "why it stopped"
>

Keyword breakdown:

Keyword What it does
export Makes the function importable from other files
async Can use await inside (handles Promises)
function* A generator — can yield multiple values over time instead of returning once
async function* Both combined: can await async operations AND yield values one at a time
Normal function:    runs → returns ONE value → done
Generator function*: runs → yields MANY values → done
Async function:     runs → awaits → returns ONE value
Async function*:    runs → awaits → yields MANY → done

AsyncGenerator<TYield, TReturn>:

Type param Position Meaning
TYield 1st Type of values coming out each time you call yield
TReturn 2nd Type of the final value when the generator is done

Inside the function body:

yield { type: "thinking", content: "analyzing..." };
yield { type: "tool_call", tool: "read_file" };
yield { type: "response", content: "Here's the answer" };

return { reason: "complete", turns: 3 };  // Terminal

Consumer side:

const agent = query(params);
const { value, done } = await agent.next();
// When done === true: value is Terminal (the return value — why it stopped)

Analogy: A normal function is a vending machine (put money in, get one item out). A generator is a conveyor belt (keeps pushing items out one by one, you take each when ready). An async generator is a conveyor belt that sometimes pauses to wait for the next item to be manufactured.

TypeScript Generics — <T>

function take<T>(n: number, gen: Generator<T>): T[] {
  const result: T[] = [];
  for (const val of gen) {
    result.push(val);
    if (result.length >= n) break;
  }
  return result;
}

<T> is a generic — a placeholder for "whatever type the generator yields." It lets one function work with any generator:

take(5, fibonacci())            // T = number → returns number[]
take(3, someStringGenerator())  // T = string → returns string[]

Without generics, you'd need separate functions per type. With generics, one function handles all. TypeScript infers T automatically from what you pass in.


Idiom of the Day

"Let the cat out of the bag" — to accidentally reveal a secret that was supposed to stay hidden.

In context: Anthropic's build pipeline let the cat out of the bag — a single overlooked .map file exposed the entire internal architecture that was meant to stay proprietary.


← Back to Daily Log Summary