The short version
- On 31 March 2026, version 2.1.88 of the Claude Code npm package shipped with a source map, which led to its full TypeScript source. Anthropic called it “a release packaging issue caused by human error”.
- Luminair's desktop app packs with an allowlist:
build.filesindesktop/package.jsonnames what goes in. Nothing else ships. - On 9 September 2026 that list left out one new file, and release 1.0.109 crashed at launch for everyone who updated. We pulled it from the update feed so new installs got 1.0.108, and shipped 1.0.110 the same day.
- A build gate now opens the packed archive, checks every local
requirein the entry points against it, and fails the build on the first miss.
One debugging file, 512,000 lines.
The Register's headline on 31 March 2026 was not gentle: “Anthropic goes nude, exposes Claude Code source by accident.” InfoQ's write-up gives the mechanics. “Version 2.1.88 of the @anthropic-ai/claude-code package shipped with a source map file that should never have been included.”
A source map is a debugging aid. When code is bundled or minified for release, the map lets tools translate an error back to the original lines. It is useful on a developer's machine. In a published package it is a pointer home. InfoQ explains that this one “referenced the complete, unobfuscated TypeScript source hosted on Anthropic's own R2 cloud storage bucket, making it directly downloadable as a ZIP archive.” It notes that Claude Code uses the Bun runtime, “which generates source maps by default unless you explicitly disable them.”
The source spanned, in InfoQ's words, “approximately 1,900 TypeScript files and over 512,000 lines of code”. On Hacker News the thread passed two thousand points. Anthropic's statement, quoted by both outlets: “a release packaging issue caused by human error, not a security breach,” with no customer data or credentials involved.
InfoQ's advice for avoiding it is short: add *.map to .npmignore, “maintain an explicit whitelist in package.json's files field, or run npm pack --dry-run before publishing to audit what gets included.” It also quotes developer and security analyst Gabriel Anhaia: “A single misconfigured .npmignore or files field in package.json can expose everything.”
The list at the top of the build.
Luminair's desktop app is an Electron app. Its code is packed into a single archive, app.asar, by electron-builder. What goes into that archive is decided by one field in desktop/package.json, build.files. It is an allowlist, and it looks like this:
"package.json", "main.js", "preload.js", "renderer.js", "slack-mirror.js", "slack-workspaces.js", "owm-controller.js", "engines/**/*", "lib/**/*", "scripts/**/*", "node_modules/**/*", // … and exclusions, which win over the lines above: "!**/.env", "!**/.env.*", "!**/*.log", "!**/.chrome-ext-keys/**", "!runner/**"
Top-level files are named one by one. A few folders (engines, lib, scripts and others) are included whole. A handful of patterns are always excluded. Anything at the top level that is not on the list stays on the developer's disk.
That is the right default for a desktop app that sits next to your code and your accounts. A stray .env, a log file or a local key folder in the working tree never reaches a user's machine by accident, because nobody would ever add it to the list on purpose.
The list is also not the only check. The package security gate in desktop/build/package-security.js opens the finished archive and refuses it if it finds a private key in any file, a .env file, key files such as .pem or .p12, a backend or server folder, or the software bill of materials file, which is published beside a release rather than inside it.
“Cannot find module”, for everyone.
The cost of an allowlist is that every new top-level file has to be added to it. Forget, and the file is silently left out.
We got a warning. On the evening of 8 September a new Slack mirror module, slack-mirror.js, was about to go out without being listed. It was caught before release. The commit that added it to the list says the staged build “would fail at launch on the require in main.js”.
The next day it happened for real. A second new module, slack-workspaces.js, was required by main.js at boot and was not on the list. Release 1.0.109 was built by CI and published. The fix commit records what users saw: “every updated install crashed on launch with "Cannot find module './slack-workspaces.js'".”
The first move was to stop the spread. Luminair's download buttons and its update feed both resolve through the “latest” release on our releases repository. Marking 1.0.109 as a pre-release made “latest” point back at 1.0.108, so new downloads and waiting updaters stopped getting the broken build.
The uncomfortable part is written plainly in the incident note: people already on 1.0.109 could not update their way out, because the crash happened before the updater could run. They had to download the app again.
It worked on the working tree.
Every local check had passed. The reason is recorded in the same note: the line adding the file to the allowlist existed on the development machine, but only as an uncommitted edit. Local builds used the working tree and packed the file. CI built from the repository, which did not have the line.
This is the general shape of packaging bugs. The development tree always has every file. It is only the packed artifact that can be missing one, and it is only the packed artifact your users run.
Read the archive, not the folder.
Six minutes after the allowlist fix, the gate went in. The function is assertPackagedLocalRequires. It opens the packed app.asar, reads main.js and preload.js out of it, and finds every literal require('./…'). For each one it tries the same candidates Node would: the exact path, then with .js, then .json, then /index.js. If none of them is in the archive, the build fails.
require('./slack-workspaces.js') slack-workspaces.js✕slack-workspaces.js.js✕slack-workspaces.js.json✕slack-workspaces.js/index.js✕resolvePackagedModule and assertPackagedLocalRequires. We did not rebuild 1.0.109; this is what the rules produce for its archive, which did not contain the file.The gate runs inside the wider package check, which is called from two places: electron-builder's afterPack hook, and the separate verify-package step. So a local build and a CI release both hit it, and a missing module now fails the build instead of the launch.
There is a second, earlier layer in desktop/test/core/package-local-requires.test.js. It reads build.files and checks that every local module main.js and preload.js require is covered by a named file or an included folder. That one runs with the test suite, before anything is packed, so the mistake shows up while you are still at your desk.
We replayed that check against the package.json and main.js from just before the fix. Out of 52 local requires in main.js, it reports exactly one uncovered: ./slack-workspaces.js.
Keep the list. Test the output.
It would be easy to read this as a case against allowlists. It is the opposite. The allowlist did its job: it kept everything unnamed out. The failure was that nothing checked the other direction, that everything needed was in.
That pairing is the takeaway for any packaged tool, whether it is an npm CLI or a desktop app:
- Name what ships. An explicit list means a debugging artifact, a secret or a stray folder has to be added on purpose to leak.
- Scan what ships for what must never ship. Keys, env files, server code. Look inside the archive, not at the folder.
- Check that what ships can run. Resolve the entry points' imports against the archive. It is a few dozen lines, and it turns a crash for every user into a failed build for one developer.
This was not the first time the finished package had lost something. A comment in the same gate file records a build on 31 August, 1.0.91, that “went out with a gutted node_modules”, with no Claude platform binary and no Codex, so every model failed. The gate has checked since then that every bundled engine is present. Each of these checks exists because a real build once got it wrong.
Checked, and not claimed.
What this post does not claim
- How many people installed 1.0.109. We do not have that number.
- That the gate catches every missing file. It covers literal
require('./…')calls inmain.jsandpreload.js. A path built at runtime, or a file loaded some other way, is not checked by it. - That the allowlist names every file. Folders such as
libandnode_modulesare included whole, so what is inside them is decided by what is in the folder. - Anything about Claude Code's leak beyond what InfoQ and The Register reported.
For another story about a change that reached everyone at once, read The deploy that closed every Mac's socket for four hours.
Sources
- The Register · 31 March 2026Anthropic goes nude, exposes Claude Code source by accident
- InfoQ · April 2026Anthropic Accidentally Exposes Claude Code Source via npm Source Map File
- Hacker News · 31 March 2026Claude Code's source code has been leaked via a map file in their NPM registry
Built with gates in front
Every Luminair release is checked inside the archive before it can reach your Mac.