// blog/how-source-maps-leak-source-code-from-npm-packages
How Source Maps Leak Source Code From npm Packages


On March 31, Chaofan Shou posted on X that Claude Code's source code had leaked through a map file in Anthropic's npm registry entry, with a link to a zip of the reconstructed source sitting on a public Cloudflare R2 bucket. The post cleared 35 million views within the week. Anthropic ships Claude Code as a minified JavaScript bundle on npm, and the product's internals (the agent loop, the prompt scaffolding, the tool implementations) have been a subject of sustained curiosity since it launched. People have been unminifying and studying that bundle for over a year. This week, a source map reconstructed the source byte for byte.
Any team shipping closed-source JavaScript can reproduce this with one default bundler flag and one missing line in package.json. A .map file can carry an entire original codebase, slip into published packages, and remain there unnoticed.
What a source map contains
Source maps exist so that a stack trace pointing at line 1, column 48,213 of a minified bundle can be translated back to src/agent/loop.ts:87. The format is a JSON file, conventionally published next to the bundle as bundle.js.map and referenced by a comment at the end of the bundle:
//# sourceMappingURL=cli.js.map
Most people remember mappings, the base64-VLQ string that maps generated positions to original positions. Leaks come from a different field: sourcesContent.
{
"version": 3,
"sources": [
"../src/agent/loop.ts",
"../src/tools/bash.ts",
"../src/prompts/system.ts"
],
"sourcesContent": [
"export async function runLoop(ctx: AgentContext) { ... }",
"...",
"..."
],
"mappings": "AAAA,SAASA,..."
}
sourcesContent is an array holding the complete, verbatim text of every original source file that went into the bundle. It holds the literal files, with comments, TODO notes, internal URLs, original variable names, and the directory structure preserved in the sources paths. If a bundle was built from 400 TypeScript files, the map contains all 400 of them. A ten-line script using the source-map library, or any of several off-the-shelf unpacker tools, writes the tree back to disk. The leak circulated as a tidy src.zip because the source was already there.
Why the source ends up embedded by default
Bundlers embed sourcesContent by default whenever source maps are enabled, because for the primary use case (debugging in a browser or an error-tracking dashboard) it is the right call. The debugger can show original source without needing access to your repository. The default creates a problem when the map reaches a place your users can read.
| Toolchain | Behavior with source maps on | How to strip embedded source |
|---|---|---|
| esbuild | --sourcemap includes sourcesContent | --sources-content=false |
| webpack | devtool: 'source-map' includes it | devtool: 'nosources-source-map' |
| Rollup / Vite | sourcemap: true includes it | output.sourcemapExcludeSources: true |
| tsc | sourceMap: true omits it | Embeds only if you set inlineSources: true |
For tsc you have to opt in to the dangerous behavior, while for the three bundlers that dominate real-world builds you have to opt out. Most teams never think about it, because in a typical web app the maps either stay out of the deploy artifact or get uploaded privately to Sentry or an equivalent and deleted from the public path. An npm-distributed CLI has no such separation. The tarball is the deploy artifact, and everything in it is world-readable by design.
A stripped map (nosources-source-map and friends) still resolves stack traces to original file names and line numbers, which is usually all you wanted from production maps in the first place. You lose inline source display in the debugger but keep your source.
How .map files sneak into published tarballs
npm decides what goes into a published tarball through a chain of rules that is easy to get wrong:
- If
package.jsonhas afilesfield, it acts as an allowlist. A pattern like"files": ["dist"]includes everything underdist/, and that meansdist/cli.js.maprides along withdist/cli.js. - With no
filesfield, npm falls back to.npmignore. - With no
.npmignore, npm falls back to.gitignore. Since build output is usually gitignored wholesale, this path rarely saves you either.
The map is generated next to the bundle, the packaging rule includes the directory rather than the file, and the map ships. npm publish prints a file list, but a .map line in a wall of output can go unnoticed.
Exclude maps in the allowlist:
"files": ["dist", "!dist/**/*.map"]
You can also stop generating full maps for release builds and inspect the tarball before it leaves your machine:
npm pack --dry-run 2>&1 | grep -E '\.map$'
tar -tzf $(npm pack --silent) | grep -E '\.map$'
Either command as a CI gate on the release job would have caught this class of leak. Grep the bundle for a sourceMappingURL comment pointing at a file you did not intend to ship; it shows that the build produced a map somewhere.
Unpublishing cannot remove leaked source
By the time Shou's post went up, the reconstructed source was already mirrored to a public object-storage bucket, and the post linking it was on its way to 35.7 million views. npm's own unpublish policy is restrictive, registry mirrors and proxy caches (Verdaccio instances, corporate Artifactory installs, CI caches worldwide) hold every version ever fetched, and any individual with the tarball can rehost it in seconds. Removing or patching the offending version only stops new downloads. Public registry publication cannot be fully retracted, so prevention has to happen at pack time in CI, before the artifact exists anywhere public.
What a leak like this costs
Minified JavaScript deters casual reading, and current language models are quite good at renaming and restructuring minified bundles back into something legible. For a tool as heavily studied as Claude Code, determined observers had already mapped a lot of its behavior from the outside, and the parts many people consider the crown jewels of an agentic CLI (system prompts, tool schemas) are substantially observable at runtime by anyone proxying their own API traffic.
The source map reduced effort and increased fidelity. Reconstructed-from-map source is the author's own code: real module boundaries, real names, real comments, and architectural decisions legible as written rather than inferred. For a company whose product's edge is partly in engineering details competitors would love to study, that is a real loss even if no credential or vulnerability ships with it. Anything inside a client-side artifact, minified or otherwise, should already be treated as public. Secrets belong on the server side of the API boundary. Source maps make public client-side code immediately readable.
Release artifacts should strip sourcesContent or disable maps. CI should fail on .map files in published tarballs, and teams should audit versions already published to npm.