bundle

Your Node app, one signed file.

Ship a Node.js app as one file with a signature over the whole thing, and install other people's apps knowing exactly what you are getting. No npm install on the other end. Node runs the app straight out of the file.

# pack your app, sign it, and it is a program
bundle create --base ./app --files app.manifest --launcher --output app.unsigned.nzip
bundle sign app.unsigned.nzip
./app.nzip

Get it Source on GitHub

Experimental. It needs Node 26.11.1 or later, run with --experimental-vfs. Everything here, Node's support included, is experimental.

What is actually running on that machine?

When you publish version 1.4 of a Node tool, you think you shipped 1.4. What you actually shipped is a recipe, and the user's machine cooks it.

  • Decided at install time, not when you released. ^1.2.0 means whatever the registry says today.
  • Installing runs code. postinstall scripts run before anyone has looked at anything.
  • Nothing records it. Two installs of the same version can differ, and nobody can tell you how.
  • And it gets attacked. Hijacked maintainer accounts and self-spreading install-script worms were both npm news in 2025.

bundle freezes the whole tree into one file, decided once, when you release. It is a real ZIP: every tool you have can open it, and unzip -l shows what is in it. One signature covers the whole file, so changing one byte anywhere means it no longer checks out. And since Node 26, node --vfs-load mounts the file and runs the app inside, with nothing extracted to disk.

What a signature proves, and what it does not

It proves where it came from

The code is the code that was signed, by whoever holds that identity. With sigstore that is your CI workflow or your login, and there is no key to keep or lose.

It does not prove it is safe

Most npm compromises shipped correctly signed packages from real, hijacked accounts. A signature would have confirmed they came from the real maintainer. That is why review is a step of its own, and why other people can vouch for a file, or warn against it.

Compared with what you use today

Questionnpm i -gDockercompiled binarybundle
Needs a registry at installyesyesnono
Runs code to installcannonono
Same bytes everywherenoyesyesyes
Open it, see what is insidesort ofwith effortnounzip
Checked before it runsnoif you set it uprarelyyes, signed whole
Sizesmallan OSa runtimetiny, or a runtime

It does not replace npm. You still develop with npm. It changes what you hand to the person at the other end.

Shipping your app: four steps

In this order. Each one is a command, and the same files always make the same archive, so anyone can rebuild yours and compare.

  1. 1 Observe

    Run your app once, the way it is normally used, and write down every file it reads. Dynamic require, data files and conditional imports are exactly what static analysis misses, and exactly what a run reads.

    BUNDLE_MANIFEST=app.manifest node --experimental-vfs -r @pipobscure/bundle/record --vfs-load=./app -- <args>
  2. 2 Create

    Archive exactly those files. --launcher puts a short shell script in front so it runs by name; it is decided here, so the review sees it.

    bundle create --base ./app --files app.manifest --launcher --output app.unsigned.nzip
  3. 3 Audit

    Review it, against your last release if there is one. Read it yourself and approve it, or let the audit skill that bundle skill installs for Claude Code read every file. bundle audit --check refuses to go on without a clean verdict over exactly these bytes.

    bundle audit --baseline last-release.nzip app.unsigned.nzip
  4. 4 Sign

    Only once the review came back clean. Signing uses sigstore by default: your CI's identity, or a browser sign-in. Your own certificate authority works too.

    bundle audit --check app.unsigned.nzip && bundle sign app.unsigned.nzip   # -> app.nzip

The result is a file you run by name, or a plain archive to mount. With bundle sea it is a standalone executable with Node inside, which checks its own signature before it runs anything. bundle itself ships this way: its release workflow tests, audits, signs with the workflow's own identity, and publishes, with no secrets.

Installing apps: see who vouches, then decide

bundle install takes a URL, a domain, or a name someone published. Before anything is put on your PATH, it shows you what vouches for what it fetched:

$ bundle install @alice.example/tool
* tool: sha256:3f1a… (signed)
   1 signed by   https://github.com/acme/tool/.github/workflows/release.yml@refs/heads/main — new
   2 attested by audit.acme.com (did:web:audit.acme.com) as audited — trusted by policy
     WARNING     scanner.example (did:plc:…) as malware — marked it bad
* tool: … accept which? numbers (1,2), 'all', or Enter to decline:
  • Nothing runs to install it. The file is fetched, checked, and moved into place. No install scripts.
  • You decide whom to trust, once. Anyone your policy trusts goes through without a question. Anyone new is your call, and your answer is remembered for updates.
  • Broken or tampered files never land. Archives that do not verify are refused outright.
  • Updates follow the publisher, and a quiet check at shell start tells you when someone has flagged something you installed.
  • Or one click. An nzip: link on a page opens bundle, with the same review in a window.

Running an app by name does not check it, because a #! launcher has no way to. bundle run app.nzip runs it through the verifying mount, and a standalone executable checks itself every time.

Who vouches for this file?

Signatures

Say who built it. With sigstore that is your CI workflow or your login.

Attestations

Anyone can vouch for a file ("I audited this", "I rebuilt it and got the same bytes") or warn against it, from their own account.

Your policy

Whom you trust, what you require, and whose warnings stop an install.

On the Atmosphere

Attestations, listings and suites are records in people's own Atmosphere (atproto) accounts, with any provider. Everyone writes only to their own account: the publisher says where the file is, a curator says what goes together, and an auditor, a scanner or your IT department says what they think of it. You hold the policy that decides whose word counts.

  • No registry to run, and nothing in the middle to compromise. An open backlink index only says whom to ask; every record is fetched from its owner and checked against their key.
  • Publish under your handle. A listing points at your release URL, such as a GitHub release, and a new version is just a new release. People bundle search for it and install it by name.
  • Attestations are checked offline, from a cache that bundle trust keeps fresh.
bundle publish --as you.example --description 'what it does' tool https://github.com/you/tool/releases/latest/download/tool.nzip
bundle attest --as you.example --kind audited app.nzip

You do not need an account to use bundle. Install by URL or domain, sign with sigstore or your own CA. Accounts are only for publishing names and attestations.

Suites and plugins

A whole toolset, one question

A suite names apps and plugins that go together, and what each must carry: a signer, attesters. bundle install @you.example/devtools installs them all, and asks you once whom to accept.

The suite vouches for nothing: each member is reviewed on its own. Updates follow it as members are added and taken out. An IT department can permit a whole suite with one attestation, and still permit or prohibit single bundles in it.

Plugins that are signed bundles too

Apps can take plugins that are bundles: signed, installed per app, reviewed like any install, and removed with the app. They load through ordinary import.

import { use } from '@pipobscure/bundle/plugins';

use('bled');
const gpio = await import('@alice/bled-gpio');

A plugin can never replace something your app already has, and it gets your app's own modules. You can require that plugins are audited as they load.

And when one product is many archives, such as services, web apps and drivers, the app pins every part by hash. Sign the app, and you have signed exactly those bytes; installing it installs all of them, or none.

What this is not

  • Not stable yet. It needs Node 26.11.1 or later with --experimental-vfs, and both sides are experimental.
  • Not a sandbox. Checked code runs with full access to your machine, like any Node program.
  • Not proof of safety. A signature says who built it, not that it is harmless. That is what review and attestations are for.
  • Not checked when run by name. Use bundle run or a standalone executable when you want the check every time.

Get it

Download bundle.nzip from the latest release, and run its install with Node 26.11.1 or later:

curl -LO https://github.com/pipobscure/bundles/releases/latest/download/bundle.nzip
node --experimental-vfs --vfs-load ./bundle.nzip install

On Windows, curl.exe -LO … in PowerShell downloads it, and the second line is the same. That run fetches the latest release, checks its signature, and puts the signed bundle on your PATH, where bundle update keeps it current. The bundle command is itself a bundle: bundle verify tells you who signed it. Then bundle --help.

For the library and the plugin loader: npm install @pipobscure/bundle.