Package manager
Nub ships its own installer with a pnpm-shaped CLI and lockfile-compatibility with whatever your project already uses — npm, pnpm, and Bun round-trip, Yarn read-only.
Nub has its own install engine: it resolves the dependency graph and links node_modules. What makes it a new kind of package manager is that it does not force incumbent projects into a Nub-only format — three properties carry the whole design:
- Bring your own lockfile. Nub installs in whatever lockfile your project already has. Under explicit Nub identity, its own canonical lockfile is
lock.yaml: a different basename, but byte-for-byte the pnpm v9 lockfile format. - Bidirectional read and write. Nub reads and rewrites the project's native lockfile: npm
package-lock.json,pnpm-lock.yaml, andbun.lockround-trip cleanly. Yarn'syarn.lockis read-only — consumed, never rewritten. - Round-trip fidelity. Read a foreign lockfile, install, and write it back, and it stays faithful — byte-for-byte where the format allows (npm
package-lock.jsonv2/v3).
Each incumbent gets its own page covering lockfile fidelity, config surfaces, and gaps: pnpm, npm, Bun, Yarn.
Nub never asks which package manager you use — it infers the incumbent from the packageManager field or the lockfile on disk and mirrors it. The CLI itself is pnpm-shaped. The engine is the vendored aube engine, embedded as a library and driven by Nub's own CLI.
You don't need to use Nub's package manager
The installer is optional. Keep running npm, pnpm, yarn, or bun exactly as you do today, and reach for Nub for everything else — running files, scripts, and binaries.
nub install
Resolves the graph and links node_modules. The verb, aliases, and flags follow pnpm:
nub install # alias: nub i
nub install --frozen-lockfile # fail if the lockfile is out of date
nub install -P # --prod / --production
nub install -D # dev only
nub install --node-linker hoisted
nub ci # clean install from the lockfileThe accepted flags are pnpm's spellings:
--frozen-lockfile / --no-frozen-lockfile / --prefer-frozen-lockfile
--prod, -P # production install
--dev, -D
--ignore-scripts
--no-optional
--offline / --prefer-offline
--lockfile-only
--force
--node-linker
--registry
--dir, -C # pnpm's spelling, not npm's --prefixnub add
Resolves a package, links it, and writes the dependency into package.json:
nub add <pkg> # alias: a
nub add -D <pkg> # --save-dev
nub add -E <pkg> # --save-exact (pin, no ^)
nub add -O <pkg> # --save-optional
nub add --save-peer <pkg> # peerDependencies
nub add -g <pkg> # global install
nub add -w <pkg> # write to the workspace root
nub add --save-catalog <pkg> # add into the workspace catalog
nub add --allow-build=<pkg> # pre-approve its build scripts for this install
nub add --no-save <pkg> # link without persisting to package.jsonnub remove
Drops a dependency from package.json and relinks node_modules:
nub remove <pkg> # rm / uninstall / un / uni
nub remove -D <pkg> # remove only from devDependencies
nub remove -g <pkg> # remove a global package
nub remove -w <pkg> # remove from the workspace rootnub update
Re-resolves dependencies within their ranges; --latest rewrites the package.json ranges to the newest resolved versions:
nub update # up — refresh all deps within range
nub update <pkg> # update a single dependency
nub update -L # --latest: move past the manifest range
nub update -E -L # pin the rewritten range to an exact version
nub update -D # devDependencies only
nub update -P # production only
nub update --lockfile-only # refresh the lockfile, leave node_modules alonenub dedupe
Collapses duplicate versions in the lockfile to fewer, shared resolutions:
nub dedupe # rewrite the lockfile with deduped resolutions
nub dedupe --check # CI: exit non-zero if dedupe would change anythingnub import
Converts another package manager's lockfile to Nub's pnpm-lock.yaml, without installing:
nub import # read package-lock.json / yarn.lock / bun.lock → pnpm-lock.yaml
nub import --force # overwrite an existing pnpm-lock.yamlThe full registered verb set covers more:
why outdated list, ls
patch patch-commit patch-remove
approve-builds prune rebuild
fetch link, unlink audit
licenses bin root
store config pkg
publish pack dlx createnub pm
The install engine is distinct from nub pm, the package meta-manager, which provisions and runs the exact pnpm/npm/yarn your project pins (corepack's job).
- For "install dependencies," this engine.
- For "fetch and run the project's pinned PM,"
nub pm.
The two compose: nub pm shim routes bare npm / pnpm / yarn through the pin while you keep using whatever installer you prefer.
Compat mode
What Nub reads and writes keys off the project's incumbent package manager — inferred from the packageManager field or the lockfile on disk:
| Package manager | Config it reads |
|---|---|
| npm | package-lock.json (v2/v3)npm-shrinkwrap.json.npmrcoverridesworkspacesengines / os / cpunpm_config_* |
| pnpm | pnpm-lock.yaml (v9)pnpm-workspace.yaml.pnpmfile.cjs.npmrcpackage.json#pnpmresolutionscatalog:workspace:workspacesengines / os / cpupnpm_config_*npm_config_*dependenciesMeta.injectednodeLinker: pnp |
| Yarn | .npmrcresolutionsworkspace:workspacespackageExtensionsdependenciesMeta.builtengines / os / cpuyarn.lock (read-only).yarnrc.yml (registry, auth, linker).yarnrc (registry, auth)YARN_* (registry, auth, linker)nodeLinker: pnp |
| Bun | bun.lock.npmrctrustedDependenciesoverridesresolutionspatchedDependenciescatalog:workspace:workspacesengines / os / cpubunfig.toml (registry, scopes, linker)bun.lockbBUN_CONFIG_* |
| nub | lock.yaml.npmrcoverridesresolutionspatchedDependenciescatalog:workspace:workspacesengines.nodenpm_config_* |
Each incumbent has its own page with the full lockfile, config, and gaps detail: pnpm, npm, Bun, Yarn.
Lifecycle scripts
Some dependencies run build steps on install — preinstall, install, and postinstall scripts declared in their own package.json. This applies across pnpm, npm, and Bun, so it lives here once. Nub does not run them indiscriminately the way npm does; you control which packages are allowed to build.
nub approve-builds # interactively approve packages to build
nub add --allow-build=<pkg> <pkg> # pre-approve a package's build scripts as you add it
nub rebuild # re-run build scripts for already-approved packages
nub install --ignore-scripts # skip every dependency build script this installWhich manifest field grants permission tracks the inferred incumbent: pnpm projects use pnpm.onlyBuiltDependencies / pnpm.allowBuilds, Bun projects use trustedDependencies, and the neutral allowBuilds field plus nub approve-builds apply in any project. An explicit denial (allowBuilds: { pkg: false }, neverBuiltDependencies) always wins. When a package wants to build but isn't allowed, Nub skips it and prints WARN_NUB_IGNORED_BUILD_SCRIPTS naming the package, with nub approve-builds as the remedy.
On top of explicit approval, a curated set of well-known packages builds automatically through a gated trust floor — see Security for the posture and the gates.
Security
Nub ships a deny-by-default install posture. Beyond the packages you approve explicitly, a curated set of well-known packages may build without approval — but only when all three gates hold at once:
| Gate | Requirement | On failure |
|---|---|---|
| Registry provenance | Resolved from a registry. Git, file, link, tarball, and npm-alias specifiers never qualify — an alias can't borrow a listed name's trust. | Not built |
| Advisory vetting | An OSV MAL-* advisory check ran against this graph, or the graph was inherited from an already-checked lockfile (a frozen install, nub ci, a teammate's clone). | Not built |
| Cooling window | The resolved version's publish time is older than minimumReleaseAge (default 24 hours). | Not built — fails closed on unknown publish time |
Explicit decisions outrank the floor in both directions. A package you approve — through the incumbent's allow-list (pnpm.onlyBuiltDependencies / pnpm.allowBuilds under pnpm, trustedDependencies under Bun), the neutral allowBuilds field, or nub approve-builds — builds regardless; an explicit denial (allowBuilds: { pkg: false }, neverBuiltDependencies) always wins.
A fresh resolve, or a lockfile Nub itself wrote (which carries the time: block), gives the floor everything it needs, so curated packages like esbuild build automatically:
# captured: nub 0.0.44, pnpm-incumbent, esbuild@0.21.5 — no allowBuilds entry
$ nub install
WARN defaultTrust: running build scripts for 1 default-trusted package(s): esbuild@0.21.5 code=WARN_NUB_DEFAULT_TRUST_BUILDS count=1 packages=["esbuild@0.21.5"] # ✓ all three gates passed
dependencies:
+ esbuild@0.21.5When a gate fails, the floor steps aside rather than guess: the same package is skipped and disclosed, with nub approve-builds as the remedy. Tighten the cooling window past every published version and even a curated package fails closed:
# captured: nub 0.0.44, esbuild — .npmrc minimumReleaseAge set past every release
$ nub install
WARN ignored build scripts for 1 package(s): esbuild@0.21.5. Run `nub approve-builds` to review and enable them. code=WARN_NUB_IGNORED_BUILD_SCRIPTS # ❌ cooling-window gate failed closed
dependencies:
+ esbuild@0.21.5A foreign lockfile that carries no publish-time data — notably an incumbent bun.lock — trips the same fail-closed path: the cooling gate has nothing to read, so the package is skipped (see the Bun page for the captured A/B).
Advisory gate
The OSV check queries api.osv.dev on a fresh resolve. A confirmed MAL-* hit is a hard block — the install aborts with ERR_NUB_MALICIOUS_PACKAGE, never a skip-and-warn, because a malicious-package advisory isn't a judgement call. An osv.dev outage is treated differently: the check fails open, warning and proceeding so a network blip can't brick an offline install. That asymmetry is deliberate — a hit always blocks, an outage never does. To fail closed on outages too, set advisoryCheck=required (also bundled into paranoid below).
Frozen reinstalls — nub ci, --frozen-lockfile, a teammate's clone — inherit the advisory vetting recorded when the lockfile was written and skip the per-install round-trip, but still enforce the cooling and provenance gates on every install.
Build jail
The OS-level build jail — a network-blocked, filesystem-scoped sandbox around every build script — is compiled in but off by default. Opt in with the neutral paranoid / npm_config_paranoid setting, which also flips the advisory gate to fail-closed. The jail covers macOS and Linux; Windows is a passthrough.
Package manager inference
Which lockfile Nub reads and writes, which config it honors, whether a write is allowed — all key off one decision: which package manager owns this project. Nub infers the incumbent and mirrors it; it never asks.
To make Nub the project owner explicitly, run:
nub pm use nubThat is the intentional identity switch. It aligns the manifest and lockfile, migrates pnpm workspace config into neutral package.json fields, and moves the project onto Nub's own config surface. If your project is already npm, pnpm, Bun, or Yarn, use the corresponding compatibility page first: pnpm, npm, Bun, Yarn.
Signals, in precedence order:
-
packageManagerfield (package.json) — the Corepack standard, and a stronger signal than any lockfile on disk. A pnpm project that picked up a straypackage-lock.jsonkeeps resolving as pnpm. -
devEngines.packageManagerfield — the fallback declaration, object ({ "name": "pnpm" }) or array form. Outranked bypackageManager: whenpackageManageris present, it decides the identity anddevEnginesisn't consulted for it. The hard error is a declaration vs. on-disk lockfile mismatch (shown below), not a disagreement between these two fields. -
The lockfile on disk — consulted only when no declaration names a manager. There is no precedence: exactly one recognized lockfile may be present, and it selects the incumbent:
pnpm-lock.yaml→ pnpmbun.lock→ Bunyarn.lock→ Yarnpackage-lock.json/npm-shrinkwrap.json→ npm
Lockfiles for two different managers are a hard error (
ERR_AUBE_LOCKFILE_AMBIGUOUS) — Nub refuses to guess which owns the project. Declare one withnub pm use <pm>, or remove the stale lockfile. -
Nothing present — a fresh project starts in pnpm-compatible shape and writes
pnpm-lock.yaml. Usenub pm use nubwhen you want Nub, rather than pnpm compatibility, to own the project.
In a workspace, only the root carries the declaration and lockfile; Nub walks up from the working directory, so a member package resolves to the root's identity.
Nub identity
A project is Nub identity when it declares Nub through nub pm use nub or when lock.yaml is the only lockfile signal. In that mode, Nub's surface is deliberately neutral:
- Lockfile:
lock.yaml, using the pnpm v9 lockfile schema byte-for-byte under Nub's own basename. - Package fields:
workspaces(array form, or object form withpackages,catalog, andcatalogs), top-leveloverridesandresolutions, top-levelpatchedDependencies, top-levelallowBuilds,engines.node, and lifecyclescripts. - Config and env: the standard
.npmrccascade,npm_config_*, neutral env such asCIand proxy variables, plus Nub's own PM knobs:NUB_CACHE_DIR,NUB_CONCURRENCY, andNUB_PRIMER_TTL. - Install state:
node_modules/.nub/,.nub-state, and the global store under Nub's own cache/data directories.
Nub identity does not read another package manager's branded project config. pnpm-workspace.yaml, .pnpmfile.cjs, .pnpmfile.mjs, the pnpm.* package.json namespace, pnpm_config_*, .yarnrc.yml, bunfig.toml, Bun trustedDependencies, and aube's AUBE_* runtime knobs are outside the Nub identity surface. dependenciesMeta.injected is also not implemented under Nub identity; nub pm use nub refuses projects that rely on injected dependencies. If you want pnpm hooks or pnpm workspace config to apply, keep the project pnpm-owned or run nub pm use pnpm.
Contradictions are loud
When signals disagree, Nub stops and reports it. Two lockfiles, no declaration:
$ nub install
Error: ERR_NUB_LOCKFILE_AMBIGUOUS
× multiple lockfiles found: pnpm-lock.yaml, package-lock.json — cannot tell
│ which package manager owns this project
help: set the declaration: nub pm use <pm> — or remove the stale lockfileA declaration whose lockfile is missing:
$ nub install # packageManager: "pnpm@9.0.0"
Error: ERR_NUB_LOCKFILE_DECLARATION_MISMATCH
× package.json declares `pnpm` (via `packageManager`), but pnpm-lock.yaml is
│ missing — found package-lock.json instead
help: set the declaration: nub pm use <pm> — or remove the stale lockfileUnder nub identity, Nub's own pnpm-lock.yaml does not silently outrank a foreign lockfile beside it — that pairing is the ambiguity error.
Inference vs the pinned PM
This inference picks the install engine's incumbent — the format Nub reads and writes. It is separate from the version nub pm provisions: the meta-manager resolves a pin (.yarnrc.yml yarnPath → packageManager → devEngines) to fetch and run an exact PM binary. Same signals, different questions: "which format do I install in?" versus "which PM binary do I run?".
Store and disk layout
Regardless of the incumbent, Nub installs through a global content-addressed store and links into an isolated virtual store — aube's scheme, under Nub's own directory names.
Global content store
Package files are deduplicated by content hash in a global store at $XDG_DATA_HOME/nub/store/v1/ (default ~/.local/share/nub/store/v1/). Every install imports from it, so a given package version lands on disk once and is shared across projects.
$ nub store path
/Users/you/.local/share/nub/store/v1Files materialize into node_modules by reflink (APFS/btrfs), hardlink (ext4), or copy — whichever the filesystem supports — so a populated tree costs little extra disk.
Virtual store
The default node_modules layout is isolated: direct dependencies sit at the top level, transitive packages link into a per-project virtual store, and phantom dependencies fail instead of resolving by accident. Nub's virtual store is node_modules/.nub/ (pnpm uses node_modules/.pnpm/) — same shape, not byte-shared, so alternating tools relinks the tree.
Passing --node-linker hoisted produces a flat, npm-style layout instead. npm, Yarn, and Bun incumbents default to hoisted; pnpm and nub-identity projects keep isolated.
Offline installs
When the global store already holds every package, a warm install relinks files from that store into node_modules via reflink (APFS/btrfs) or hardlink (ext4) — no re-download, no byte-for-byte copy. This is why warm reinstalls are significantly faster: the store avoids redundant I/O regardless of which layout (isolated or hoisted) the project uses.
nub install # offline when the store already holds every package
nub install --offline # force offline
nub install --prefer-offline # try the cache firstPackage runnernubx
Run a CLI from your project's installed binaries, falling back to a registry fetch when it isn't installed — a drop-in for npx and pnpm dlx, an order of magnitude faster on cold start.
pnpm
pnpm is the package manager Nub mirrors most closely — native version-9 lockfile read and write, an isolated dependency tree, and pnpm's hook file, all gated on pnpm being the incumbent.