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
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.0means whatever the registry says today. - Installing runs code.
postinstallscripts 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
| Question | npm i -g | Docker | compiled binary | bundle |
|---|---|---|---|---|
| Needs a registry at install | yes | yes | no | no |
| Runs code to install | can | no | no | no |
| Same bytes everywhere | no | yes | yes | yes |
| Open it, see what is inside | sort of | with effort | no | unzip |
| Checked before it runs | no | if you set it up | rarely | yes, signed whole |
| Size | small | an OS | a runtime | tiny, 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 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 Create
Archive exactly those files.
--launcherputs 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 Audit
Review it, against your last release if there is one. Read it yourself and approve it, or let the audit skill that
bundle skillinstalls for Claude Code read every file.bundle audit --checkrefuses to go on without a clean verdict over exactly these bytes.bundle audit --baseline last-release.nzip app.unsigned.nzip -
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 searchfor it and install it by name. - Attestations are checked offline, from a cache that
bundle trustkeeps 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 runor 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.
- Source and README on GitHub
- Command reference: every command and option
- API reference: the library and the plugin loader
- How it works: the format, what is signed, the trust model
- @pipobscure/bundle on npm