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 — it reads and rewrites the project's native lockfile, so npm, pnpm, and Bun round-trip cleanly and Yarn is consumed read-only. The CLI is pnpm-shaped; the engine is the vendored aube engine, embedded as a library and driven by Nub's own CLI.

Nub never asks which package manager you use — it infers the incumbent and mirrors it. Each one has its own page covering lockfile fidelity, config surfaces, and gaps: pnpm, npm, Bun, Yarn.

Compatibility

Run Nub in a repo that already uses npm, pnpm, Yarn, or Bun and it behaves as that package manager — no migration, no new files. Nub infers the incumbent, then mirrors it: same lockfile format, same config files, same manifest fields. Inference walks one precedence chain:

  • packageManager — Corepack standard
  • devEngines.packageManager — object or array form
  • lockfile on disk

In a workspace, the chain runs from any member: Nub walks up to the root, which carries the declaration and lockfile. Two lockfiles for different managers, with no declaration, is a hard error.

IncumbentLockfileRound-trip
npmdocs →package-lock.json, npm-shrinkwrap.jsonread + write
pnpmdocs →pnpm-lock.yaml (v9)read + write
Yarndocs →yarn.lockread-only
Bundocs →bun.lockread + write
Nubdocs →lock.yaml (pnpm v9 bytes)read + write

A no-churn guard leaves a graph-equal lockfile untouched, and Nub never drops its own lockfile into a project it doesn't own. The bun.lockb binary format is rejected — convert to text bun.lock first.

Config it reads

Config reads are symmetric with the lockfile: under each incumbent Nub reads that tool's branded config and no other's. The neutral .npmrc cascade and npm_config_* are read under every incumbent. Hover a partial chip for the breakdown; each chip is grounded in the detailed table on that incumbent's page.

Package managerConfig it reads
npmpackage-lock.json. Supported. lockfileVersion 1 errors — re-lock under npm 7+ first.npm-shrinkwrap.json. Supported.npmrc. Supportedoverrides. Supportedworkspaces. Supportedengines / os / cpu / libc. Supportednpm_config_*. Supported. Registry-client keys only.
pnpmpnpm-lock.yaml. Supported. v6/v5.4 declined — re-lock under pnpm 9+.pnpm-workspace.yaml. Supported.pnpmfile.cjs. Supported.npmrc. Supportedpackage.json#pnpm. Supportedpnpm.overrides. Supportedpnpm.packageExtensions. Supportedpnpm.patchedDependencies. Supportedresolutions. Supportedcatalog:. Supportedworkspace:. Supportedworkspaces. Supportedengines / os / cpu. Supportedpnpm_config_*. Supported. Generic settings under any pnpm version; registry-client keys (registry, proxy, strict-ssl) under pnpm v11+.npm_config_*. SupporteddependenciesMeta.injected. Partially supported. Not honored under Nub-identity migration.nodeLinker. Partially supported. No PnP — node-linker=pnp errors.
Yarnread-only.npmrc. Supportedresolutions. Supportedworkspace:. Supportedworkspaces. SupportedpackageExtensions. SupporteddependenciesMeta.built. Supportedengines / os / cpu. Supportedyarn.lock. Partially supported. Read-only; writes refused..yarnrc.yml. Partially supported. Not read: per-host proxies, PnP..yarnrc. Partially supported. Registry and auth keys only.YARN_*. Partially supported. Reads the registry, auth token/ident, node-linker, CA file, proxy, and strict-SSL env values; map-shaped and scoped env config is not translated.nodeLinker. Partially supported. No PnP — warns and links node_modules instead.
Bunbun.lock. SupportedtrustedDependencies. Supportedoverrides. Supportedresolutions. SupportedpatchedDependencies. Supportedcatalog:. Supportedworkspace:. Supportedworkspaces. Supportedengines / os / cpu. Supportedbunfig.toml. Partially supported. [install] section only.BUN_CONFIG_*. Partially supported. Registry and token only.bun.lockb. Not supported. Binary lockfile rejected — convert to bun.lock text first.

Under its own identity, Nub reads only neutral, cross-tool config — never another manager's branded fields:

Package managerConfig it reads
nublock.yaml. Supported.npmrc. Supportedoverrides. Supportedresolutions. SupportedpatchedDependencies. Supportedcatalog:. Supportedworkspace:. Supportedworkspaces. Supportedengines.node. Supportednpm_config_*. Supported

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 identity

A project is Nub's own in three cases: it declares Nub through nub pm use nub, lock.yaml is the only lockfile signal, or a fresh project has no declaration and no lockfile. The switch aligns the manifest and lockfile, migrates pnpm workspace config into neutral package.json fields, and moves the project onto a neutral surface:

  • Lockfile: lock.yaml — the pnpm v9 schema byte-for-byte, under Nub's own basename
  • Package fields: workspaces, overrides, resolutions, patchedDependencies, allowBuilds, engines.node, scripts
  • Config and env: the .npmrc cascade, npm_config_*, neutral env (CI, proxies), plus NUB_CACHE_DIR, NUB_CONCURRENCY, NUB_PRIMER_TTL
  • Install state: node_modules/.nub/, .nub-state, and the global store under Nub's own directories

This is the brand boundary in both directions: Nub never emits its own brand into your config, and under its own identity reads only neutral, cross-tool config — never pnpm-workspace.yaml, .pnpmfile.cjs, the pnpm.* namespace, pnpm_config_*, .yarnrc.yml, bunfig.toml, Bun trustedDependencies, or aube's AUBE_* knobs. Injected dependencies are unsupported under Nub identity — nub pm use nub refuses a project that relies on dependenciesMeta.injected. To keep pnpm hooks or workspace config active, stay pnpm-owned or run nub pm use pnpm.

# captured: a Nub-identity project (lock.yaml present) with a stray pnpm-workspace.yaml
$ nub install
nub: pnpm-workspace.yaml is not read under nub identity — migrate it (`nub pm use nub`), delete it, or return to pnpm (`nub pm use pnpm`).

Contradictions are loud

When signals disagree, Nub stops rather than guess. 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 lockfile

A 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 lockfile

Under Nub identity, lock.yaml beside a foreign lockfile 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 yarnPathpackageManagerdevEngines) 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?".

Compat mode

Runtime augmentation is reversible. Passing --node, or setting a truthy NODE_COMPAT for a whole tree, runs the project's pinned Node vanilla — version provisioning stays on, augmentation comes off. See the runtime overview for the full contract.

CLI

The install engine is one CLI — pnpm's verbs and flags, driven by Nub.

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 lockfile

The 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 --prefix
--reporter <name>     # default, append-only, silent
--silent, -s          # alias for --reporter=silent
--loglevel <level>    # debug, info, warn, error, silent

To quiet the progress output, pass --silent (or -s, or --reporter=silent): nothing reaches stderr but a fatal error, matching pnpm install --silent. The --reporter=append-only form drops the live progress display while keeping the dependency summary, and --loglevel error hides warnings without touching the rest. These spellings apply to every install-family command — install, ci, add, remove, update, dedupe, import — and work either after the command (nub install --silent) or before it (nub --silent install).

In a workspace, install and ci accept the same selector flags as script running — install only the packages a filter matches:

--filter <sel>, -F        # pnpm's selector grammar (see /docs/run#--filter)
--recursive, -r           # every workspace package
--filter-prod <sel>       # selector, production deps only
--include-workspace-root  # add the root package to the recursive set
--fail-if-no-match        # error if the filter selects zero packages

nub 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 + devDependencies (pnpm parity)
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.json

nub 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 root

nub 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 alone

nub 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 anything

nub 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.yaml

The 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          create

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

Lifecycle scripts

Some dependencies run build steps on install — preinstall, install, and postinstall scripts declared in their own package.json (across pnpm, npm, and Bun). Nub ships a deny-by-default posture: it does not run them indiscriminately the way npm does. You control which packages 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 install

Which 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. A package that wants to build but isn't allowed is skipped, with WARN_NUB_IGNORED_BUILD_SCRIPTS naming it and nub approve-builds as the remedy.

Default-trust floor

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:

GateRequirementOn failure
Registry provenanceResolved 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 vettingAn 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 windowThe 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.5

When 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.5

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

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/v1

Files materialize into node_modules by reflink (APFS/btrfs), hardlink (ext4), or copy fallback — 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. The npm, Yarn, and Bun incumbents default to hoisted; pnpm and Nub-identity projects keep isolated.

Offline installs

Like pnpm, Nub relinks files from the global store into node_modules via reflink (APFS/btrfs) or hardlink (ext4) when the store already holds every package — no re-download, no byte-for-byte copy. The benchmark below measures the warm reinstall case: node_modules is removed, packages are already on disk, and the installer rebuilds the project layout.

warm reinstall · create-t3-app · macOS

nub install1122 ms
bun install1444 ms · 1.29× slower
pnpm install2847 ms · 2.5× slower
npm ci4163 ms · 3.7× slower

View benchmark →

Warm reinstall, not cold

These numbers are the warm-reinstall case — a populated store and an existing lockfile, with node_modules cleared — where the relinking path is the whole cost. A cold install (empty store, fetching from the registry) is a different workload, and Nub does not lead there.

nub install            # offline when the store already holds every package
nub install --offline  # force offline
nub install --prefer-offline  # try the cache first