← Blog Building Luminair 9 September 2026 8 min read

What ships in the bundle: an allowlist, and the release it crashed

A packaging mistake can go two ways. Ship too much, and you publish files you never meant to, which is how Claude Code's source reached the public in March 2026. Ship too little, and the app does not start, which is what our allowlist did to release 1.0.109 in September. This is the story of the second one, and the check that now reads every packed archive before it can go out.

Fig 01  Two ways to get packaging wrongSchematic
SHIP UNLESS EXCLUDED Too much PROJECT FOLDER cli.js package.json README.md cli.js.map a debugging file, never meant to be published PUBLISHED PACKAGE everything above, including the map, which points at the full original source Claude Code 2.1.88 on npm, 31 March 2026 SHIP ONLY WHAT IS NAMED Too little DESKTOP FOLDER main.js preload.js slack-mirror.js slack-workspaces.js filled = listed in build.files hollow = not listed APP.ASAR main.js asks for a file that is not there, so the app stops at launch Luminair 1.0.109, 9 September 2026 THE FIX FOR THE RIGHT-HAND CASE Read the packed archive, resolve every local require, fail the build on a miss.
File names on the left are illustrative; the published facts are from InfoQ and The Register. File names on the right are real: slack-mirror.js was added to the list the evening before; slack-workspaces.js was not.

The short version

  1. 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”.
  2. Luminair's desktop app packs with an allowlist: build.files in desktop/package.json names what goes in. Nothing else ships.
  3. 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.
  4. A build gate now opens the packed archive, checks every local require in the entry points against it, and fails the build on the first miss.
02Shipping too much

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.”

An ignore list says what to leave out. An allowlist says what to put in. They fail in opposite directions.
03Nothing ships unless named

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:

desktop/package.json · build.files (excerpt)today
"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.

04Shipping too little

“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'".”

Fig 02  8 to 9 September 2026Real timestamps from git · UTC
8 SEP 15:38 1.0.108 released 9 SEP 00:05 slack-mirror.js listed in time 15:18 1.0.109 no slack-workspaces.js updated installs crash at launch marked pre-release: feed falls back to 1.0.108 18:48 bump to 1.0.110 18:49 file listed 18:55 gate committed The pre-release change has no git timestamp; its position is approximate.
Times from the release commits for 1.0.108, 1.0.109 and 1.0.110 and from the two allowlist fixes and the gate commit. The incident note in our Journal vault records the feed fallback and the order of the response. How long any single user ran 1.0.109 is not known.

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.

05Why the laptop said fine

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.

The rule we tookCheck the thing you ship, not the folder it came from. A test that reads the source tree cannot see what the packer left out.
06The gate

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.

Fig 03  The gate on the 1.0.109 packageResolution rules from package-security.js
main.js asks forrequire('./slack-workspaces.js') 
exactslack-workspaces.js✕
+ .jsslack-workspaces.js.js✕
+ .jsonslack-workspaces.js.json✕
+ /index.jsslack-workspaces.js/index.js✕
Build stops withpackage module gate: local module missing from app.asar (add it to build.files): main.js requires ./slack-workspaces.js
The four candidates and the error text are from 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.

07Allowlist, plus a check

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.

08What we checked

Checked, and not claimed.

3/3
Tests in package-local-requires.test.js pass today
1/52
Local requires in the pre-fix main.js the source check flags: slack-workspaces.js
6 min
Between the allowlist fix and the gate commit
2
Places the packed gate runs: afterPack and verify-package

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 in main.js and preload.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 lib and node_modules are 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

Built with gates in front

Every Luminair release is checked inside the archive before it can reach your Mac.

Download Luminair →