Claude Code Source Map Leak: How a .map File Reached npm, and the Release Checks That Stop It

On March 30 at 22:36 UTC, version 2.1.88 of @anthropic-ai/claude-code was published to npm with a 59.8 MB source map inside. A source map can carry the original source of the code it describes, and press reports say this one exposed the TypeScript source of Anthropic's command-line coding agent: about 1,900 files and more than 512,000 lines. Anthropic told reporters it was "a release packaging issue caused by human error, not a security breach", and that no customer data or credentials were involved.

I have not looked at the leaked code, and I will not describe it. What interests me is how a file like that reaches a public registry, because nothing about the mistake is specific to Anthropic, and the checks that stop it are cheap. The registry data and GitHub records below are primary sources. Details of the leak itself are as reported by the outlets I name, and my own arithmetic is marked.

The Claude Code 2.1.88 timeline, from publish to takedown

Time (UTC)What happened
March 30, 22:362.1.88 is published to npm
March 31, 08:23A post on X links to the exposed code (4:23 a.m. Eastern, per CNBC)
March 31, 14:35The 2.1.88 entry is removed from the public changelog
March 31, by 20:29heise reports the version already removed from the registry
March 31, 23:11Anthropic's DMCA notice appears in GitHub's public record, processed against a fork network of about 8,100 repositories
March 31, 23:322.1.89 is published
April 1, 13:14A partial retraction: one repository and 96 listed forks stay down, the rest are reinstated

Source: npm registry metadata, the anthropics/claude-code commit history and GitHub's public DMCA repository, with the X post time from CNBC and the removal from heise. GitHub times are when the change was recorded.

By my arithmetic, the version was public for under 22 hours. Most of its release notes came back under 2.1.89: 28 of the 41 lines, by my count of the changelog. That is the only way back. npm never allows a name and version to be used twice, even after removal, so a bad release cannot be replaced, only superseded.

How a source map ends up in an npm package

A source map is a JSON file that lets debuggers translate positions in bundled, minified JavaScript back to the original files. The format, standardised as ECMA-426, has an optional sourcesContent field that embeds the original source text itself. A map with that field filled in is not a pointer to the source. It is the source.

Bundlers write maps next to the bundle, so a build that produces cli.js can also produce cli.js.map. Whether that file gets published is decided by npm's packing rules, which are looser than they look:

  • No files field means everything. npm includes every file in the package directory except what .npmignore excludes, or .gitignore when there is no .npmignore.
  • A directory in files means everything in it. npm walks listed directories recursively, so "files": ["dist"] publishes dist/cli.js.map next to dist/cli.js.
  • A root .npmignore cannot take back what files lets in. npm's documentation says a root-level .npmignore does not override the files field, so adding *.map there does nothing for files the allowlist already includes.

None of this tells us how Anthropic's build produced the file. Boris Cherny, who leads Claude Code, wrote on X that a manual deploy step should have been better automated, and that the team has already made improvements to that automation, as ITPro reported on April 1. Which step, and how it let the map through, has not been published, and I am not going to guess. The lesson does not depend on the details. A release step that relies on a person getting it right fails on the day they do not, and a gate in CI does not have off days.

Three release checks that stop a map file reaching npm

In March I argued that AI tools are compressing the mechanical cost of implementation, and that the engineering lives in the decisions around the code. A release gate is one of those decisions. Each check below runs in CI and fails the release before anything is public.

1. Allowlist files, not folders

Name what you ship by pattern, such as "files": ["dist/*.js"] rather than ["dist"]. An allowlist only deserves the name if a new file in the build output does not quietly join the package.

2. Gate on npm pack --dry-run

npm pack --dry-run --json works out the exact tarball without writing it and reports every path and size, plus the file count, packed size and unpacked size. That is enough for a gate that fails on any .map path, or on any path not on an expected list. With jq, the map rule fits on one line: npm pack --dry-run --json | jq -e '[.[0].files[].path | select(endswith(".map"))] == []' exits non-zero when a map is present.

3. Diff against the last release

The registry already records what you shipped last time. npm view @anthropic-ai/claude-code@2.1.87 dist.unpackedSize dist.fileCount returns 46,604,198 bytes and 19 files. A gate that compares the new tarball with those numbers, and fails on any new file or on growth past a set percentage, needs no knowledge of what is inside. VentureBeat put the map alone at 59.8 MB, more than the whole of 2.1.87 unpacked. By my arithmetic, adding it to a package of that size more than doubles it, assuming the rest stayed similar, as the 43.0 MB of 2.1.89 suggests.

One habit closes the gap between the tarball you checked and the one you shipped: pack once, inspect that file, and publish that file. npm publish accepts a tarball as well as a folder, and the registry stores a sha512 integrity hash for every version, so a step after publishing can confirm that the registry holds the file the gate approved.

The release process for MerlinX rests on the same idea. Releases ship in gated waves: UAT runs the identical image artifacts, verified by digest rather than rebuilt look-alikes, and nothing promotes until post-deploy assertions pass and a soak holds against the baseline error rate. Waves ship rollback-free, including unattended overnight runs, and an unattended run cannot depend on someone remembering a step. The npm version has one awkward difference. Publishing under a tag other than latest keeps a version away from default installs, but anyone can still install it by number. A tag is not a staging area, so for a leak the gate has to run before publish.

Removing a version does not recall it

npm lets an owner unpublish a version within 72 hours if nothing else in the registry depends on it. That stops new installs. It does nothing about copies already downloaded, cached or mirrored, and those had spread. GitHub processed Anthropic's DMCA notice against an entire fork network of about 8,100 repositories, because the notice said most forks infringed as much as the parent. Anthropic told TechCrunch that the named repository sat in a fork network connected to its own public Claude Code repository, and on April 1 it retracted the notice for all but one repository and the 96 forks it had listed.

Leaks also become bait. Zscaler reported on April 1 that a GitHub repository posing as the leaked source delivered the Vidar infostealer and GhostSocks proxy malware instead. It is the move the malicious ClawHub skills made in February: wrap malware in something people are already looking for.

The axios compromise the same night: who published, not what

Less than two hours after 2.1.88 went out, by my arithmetic, a different failure began. Google's Threat Intelligence Group reports that between 00:21 and 03:20 UTC on March 31, axios 1.14.1 and 0.30.4 were published with a malicious dependency, plain-crypto-js, that installed a remote access trojan. GTIG attributes it to UNC1069, a North Korea-nexus group. In a post-mortem today, the lead maintainer writes that targeted social engineering put malware on his machine and gave the attacker his npm credentials.

By the accounts of the people involved, the two incidents have different causes, and they fail on opposite sides. Claude Code shipped more of its own code than intended; axios shipped someone else's code through a legitimate account. Content checks catch the first. The second needs control over who can publish: npm's trusted publishing, which uses OIDC from CI instead of long-lived tokens and adds provenance automatically, plus a gate that flags a new dependency in a patch release. The axios maintainers list immutable releases and OIDC publishing among their changes.

What I would test in a release pipeline this week

  • Plant a map file. Drop a .map into the build output on a test branch and confirm the release fails. A check that has never failed has never been tested.
  • Replay the last release. Run the dry run on the commit you last shipped and compare it with the registry's dist.fileCount and dist.unpackedSize.
  • Follow the tarball. Confirm the publish job uploads the file the gate inspected, not a fresh pack.
  • Count who can publish. List every token and person with publish rights, move CI to trusted publishing, and remove what you cannot justify.
  • Rehearse the cleanup. Know who can unpublish inside the 72-hour window, and who owns takedowns, before you need either.

A source map exists so that anyone holding it can read the original code. This week, it worked as designed.


Sources

Written by Eslam Hamed, a Technical Lead and AI Solutions Architect working on enterprise RAG, agentic AI, and release engineering for AI platforms.

Putting agents near real systems and data? I help teams design the controls before something goes wrong. See how we can work together.

Back to all posts

More posts