Script runner
A drop-in for pnpm run and npm run — every flag carries over with the same spelling and semantics, 24× faster on the cold path, with the full workspace and lifecycle-hook surface preserved.
A drop-in for pnpm run and npm run, nub run carries every flag over with the same spelling and semantics, including the recursive and workspace-filter ones:
# resume an interrupted recursive run mid-graph
nub run -r --resume-from @scope/pkg build
# machine-readable per-package results for CI
nub run -r --reporter=ndjson build
nub run -r --stream --reporter-hide-prefix build
nub run -r --workspace-concurrency 4 build
# skip members without the script
nub run -r --if-present testIt reads your package.json directly — 24× faster on the cold path — and package management stays with whatever you already use (pnpm, npm, yarn, bun).
A few flags behave differently. The --report-summary flag streams results live via --reporter=ndjson rather than writing a static file. The --if-present flag is workspace-scoped: it skips members missing the script during a recursive run. The legacy npm flags --scripts-prepend-node-path, --unsafe-perm, and --npm-path are not accepted.
nub run <script>
Pass the script name after run. The name must match a key in your package.json's "scripts" field.
nub run build
nub run dev
nub run testRunning a script is always nub run <script> — barewords like nub dev do not fall through to scripts. A nub build where build is a script fails with a hint to run nub run build. See Running files for how nub <file> works.
Dispatch is handled in Rust with no Node bootstrap in the wrapper, so the runner adds only a few milliseconds before your script's first byte — faster than Node's own node --run:
script dispatch · warm · 50 runs
Multiple scripts
A /regex/ selector in place of a script name runs every script whose name matches, mirroring pnpm run. This is the in-package equivalent of npm-run-all's run-p — without the extra dependency or the per-task PM re-spawn, since Nub spawns each script body directly.
nub run "/^build:/" # build:js, build:css, build:types — concurrentlyMatching scripts run concurrently by default, capped at min(4, CPU count), with each line prefixed by its script name. To run them one at a time instead (the run-s behavior), cap the concurrency at one:
# one at a time, in package.json order
nub run --workspace-concurrency 1 "/^build:/"The selector must be a slash-delimited regular expression literal (/^build:/); a plain build:* is treated as a literal script name, not a glob. Regex flags are not supported. An exact script name (no slashes) always runs that single script unchanged.
Equivalents for npm-run-all / npm-run-all2:
| npm-run-all | Nub |
|---|---|
run-p build:js build:css | nub run "/^build:(js|css)$/" |
run-p "build:*" | nub run "/^build:/" |
run-s "build:*" | nub run --workspace-concurrency 1 "/^build:/" |
Selecting an arbitrary, unrelated set of scripts (for example build, lint, test together, which share no name pattern) is not yet supported — track #102.
Read the full docs for pnpm's regex selector on pnpm.io.
Argument forwarding
Trailing arguments after the script name are passed straight through to the script. You do not need the -- separator that npm run requires.
nub run test --watch # forwards --watch to the test script
nub run build --target=esnext # forwards --target=esnextThis matches bun run. The explicit separator is also accepted:
nub run test -- --watch # identical result; the -- is optionalA flag before the script name (nub run --filter core test) is Nub's; a flag after the script name (nub run test --filter) is the script's. So Nub-side flags always go before <script>:
nub run --silent build # --silent is Nub's (suppresses the preamble)
nub run build --silent # --silent is forwarded to the build scriptLifecycle hooks
Nub runs pre<script> and post<script> hooks automatically, matching npm run semantics. Given these scripts:
{
"scripts": {
"prebuild": "rimraf dist",
"build": "tsc -p .",
"postbuild": "cp README.md dist/"
}
}A single nub run build runs all three in order:
nub run build # runs prebuild, then build, then postbuildTo skip the pre / post hooks (CI, or a security-conscious run), pass --ignore-scripts.
nub run --ignore-scripts buildDependency freshness
Before a script runs, Nub checks the installed node_modules against your package.json. When a dependency is missing or its installed version has drifted out of the declared range, Nub warns and still runs, so a stale tree surfaces as a clear message instead of a raw husky: command not found:
nub: dependencies may be out of date (`chalk` is not installed).
Run `nub install`.The check works whatever installed the tree — npm, pnpm, Yarn, Bun, or Nub itself. An ambiguous tree (Yarn PnP, an unparseable version, or a production-only install with devDependencies deliberately omitted) never warns.
Set the behavior with the verify-deps-before-run key in .npmrc. The values are warn (the default — warn, then run), error (print the message and refuse to run), and off (skip the check):
verify-deps-before-run = warnA project nub.jsonc sets the same policy under verifyDeps, where skipping the check is spelled false:
{
// ...
"verifyDeps": "error" // stop rather than run against a stale tree
}The NUB_VERIFY_DEPS variable overrides the file for one shell, and --no-check (or --no-install on nub run) skips the check for a single invocation:
nub run --no-check build
NUB_VERIFY_DEPS=off nub run buildNub does not install here; run nub install when the warning appears. pnpm's install and prompt values are read from .npmrc and treated as warn; in nub.jsonc they are rejected. The check also applies to nub <file>, nub exec, and nubx, and is skipped in --node / compat mode and inside an already-running script (so a script that spawns node doesn't re-check).
The one exception is a tree Nub installed for a different Node major than the one about to run it. Native addons are compiled against a specific Node ABI, so switching Node — bumping .nvmrc or engines.node, or moving between installed versions — leaves them unloadable, and the run would otherwise fail with ERR_DLOPEN_FAILED … NODE_MODULE_VERSION inside a dependency. Nub records which engine built the tree, detects the mismatch, and reinstalls once before the run:
nub: dependencies were installed for Node 22, this run uses Node 26.
Reinstalling — native addons are built per Node major.Because Nub keeps a build per engine in its store, the reinstall is a fast re-link once you have run under that Node before; only an engine it has never built for rebuilds. Setting off disables this repair along with the rest of the check.
Read the full docs for pnpm's verifyDepsBeforeRun setting on pnpm.io.
Undeclared dependencies
Nub also flags a phantom dependency — a package your own source imports but never declares, that resolves today only because some dependency-of-a-dependency hoisted it into node_modules. It works under a flat npm or Yarn install and breaks when the tree is installed isolated, so Nub names the file and the fix before that happens:
nub: src/index.ts imports `ansi-styles`, which isn't in package.json.
Run `nub add ansi-styles` (WARN_PHANTOM_DEP).The scan is conservative — it warns only on a package that is imported unguarded (a try/catch optional load is left alone), is absent from every dependency field, and is present in node_modules. It is warn-only and never changes the exit code.
Turn it off with the phantom-check key in .npmrc, or the NUB_PHANTOM_CHECK variable for one shell:
phantom-check = offThe npm_* environment
Nub populates the child process with the full npm-compatible environment, so scripts and tooling that read npm_* variables behave as they do under npm run. Locally installed CLIs are on PATH via the node_modules/.bin chain, so you can call them by bare name.
{
"scripts": {
"build": "tsc && echo \"built $npm_package_name@$npm_package_version\""
}
}nub run build # tsc resolves from node_modules/.bin; npm_* vars are setThe injected set includes:
npm_lifecycle_event
npm_lifecycle_script
npm_execpath
npm_node_execpath
npm_command
npm_config_user_agent
npm_package_name
npm_package_version
npm_package_json
npm_package_config_* # per-package keys from package.json#config
npm_config_registry # the resolved registry from the .npmrc chain
npm_config_node_gyp # path to a runnable node-gyp, for native builds
INIT_CWD # the directory you invoked `nub run` fromInside a script, spawning node (by shebang or child_process.spawn("node", …)) re-enters Nub through the PATH shim, so child processes stay transpiled and augmented — see --node below.
Read the full docs for npm's script environment on docs.npmjs.com.
The script shell
Nub runs every script body through a POSIX sh, so one script behaves the same on macOS, Linux, and Windows — rm -rf dist && mkdir dist, NODE_ENV=production node build.js, ${PORT:-3000}, $(…), &&, and pipes all work everywhere. On macOS and Linux that is the system /bin/sh; Windows has no POSIX shell, so Nub ships a small one (busybox) next to its binary and uses it by default instead of cmd.exe.
It is POSIX sh, not bash: bash-only extensions such as [[ … ]], arrays, and ${VAR^^} are not guaranteed — the same contract npm gives, whose sh -c is dash on Debian and Ubuntu.
Dependency install scripts run through the same shell during nub install. A package whose postinstall calls ./build.sh works on Windows as it does on macOS and Linux.
--script-shell
To pin a specific shell, set script-shell in .npmrc and Nub invokes script bodies with it on every platform. The rest of .npmrc (registry, auth, node-linker, hoist-pattern) is installer configuration and is not read here.
script-shell = /bin/bashYou can also override the shell per invocation:
nub run --script-shell /bin/bash buildRead the full docs for pnpm's scriptShell setting on pnpm.io.
-r, --recursive
In a monorepo, -r (or --recursive) runs the script in every workspace package. Nub reads workspace topology from package.json's "workspaces" field (npm / Yarn / Bun) or pnpm-workspace.yaml (pnpm).
nub run -r build # run "build" in every workspace package
nub run --recursive test # --recursive is the long form
nub run --workspaces lint # --workspaces is the npm-style alias for -rPackages run in topological order by default: a package's dependencies build before it does. Workspace discovery walks up from your current directory to find the workspace root, so these commands work from anywhere inside the monorepo.
--filter
The --filter flag (or -F) takes pnpm's filter grammar verbatim. Select by exact name, scope glob, name wildcard, or path glob.
nub run --filter @org/api dev # one package by name
nub run --filter "@org/*" build # every package in a scope
nub run --filter "*-utils" test # name wildcard
nub run --filter "./packages/*" lint # path glob
nub run --filter '!@org/legacy' build # exclude a packageMultiple --filter flags compose as a union of the matched sets:
nub run --filter @org/api --filter @org/web build--filter "@org/web..." — graph selectors: the dependency-graph filter forms walk the workspace package graph. Use the ... syntax to pull in related packages.
nub run --filter "@org/web..." build # @org/web + its dependencies
nub run --filter "...@org/web" build # @org/web + dependents (upstream)
nub run --filter "@org/web^..." build # @org/web's dependencies only
nub run --filter "...^@org/web" build # @org/web's dependents only--filter "[ref]" — changed since: the [ref] filter form selects packages with changes since a git ref — the common CI pattern for testing only what moved.
nub run --filter "[main]" test # packages changed since main
nub run --filter "[HEAD~1]" build # packages changed since HEAD~1
# changed packages AND their dependents
nub run --filter "...[origin/main]" testRead the full docs for the filter grammar on pnpm.io.
--parallel, --sequential
Across a workspace, Nub runs packages concurrently up to a cap (CPU count by default) while respecting topological order. Two flags adjust that.
nub run -r --workspace-concurrency 4 build # cap at 4 concurrent scripts
nub run -r --parallel dev # all at once, no order or cap
nub run -r --sequential migrate # one at a time, no orderUse --parallel for long-running dev servers where there is nothing to order, and --sequential for scripts that must not overlap.
--no-bail
By default a workspace run bails on the first package that fails (--bail). To run every package regardless and report failures at the end, use --no-bail.
nub run -r test # default: stop at the first failing package
nub run -r --no-bail test # every package, failures reported at the end--resume-from
When a long topological run fails partway through, --resume-from skips the topological predecessors of a package to restart from the failing one.
nub run -r --resume-from @org/api build-w, --workspace-root
The -w flag (pnpm's --workspace-root) targets only the root project, regardless of where you invoke it from. To run recursively across members and also include the root package, use the npm-style --include-workspace-root.
nub run -w lint # "lint" at the workspace root
nub run -r --include-workspace-root lint # all members PLUS the rootThe -w flag is pnpm's boolean root selector, not npm's member selector. To select a member the npm way, use the long --workspace <name> (repeatable) or --filter <name>.
nub run --workspace @org/api --workspace @org/web build--stream, --aggregate-output
On a TTY, Nub streams interleaved output, leading each line with the package directory and the script name (packages/api build: <line>, matching pnpm -r). On CI / non-TTY it buffers per package and flushes on finish. Override either default explicitly.
nub run -r --stream build # force interleaved live output
nub run -r --aggregate-output build # force per-package buffering
nub run -r --reporter ndjson build # one JSON event per line, for CITo drop the per-line label — so a CI annotation matcher reads the script's own error: file:line output — add --reporter-hide-prefix. Nub's own framing keeps its label, so you can still tell which package finished:
nub run -r --stream --reporter-hide-prefix buildAnd --silent (or -s) suppresses Nub's $ <command> preamble (it does not suppress the script's own stdout):
nub run --silent build--color
Prefixing each line means piping the script's output, and a piped tool sees no terminal, so most of them switch their own color off. Pass --color and the scripts keep their colors under prefixing:
nub run -r --parallel --color=always dev # scripts keep their colors
nub run -r --no-color build # plain text from Nub and scriptsNub passes the choice down as FORCE_COLOR, the variable those tools already read — 1 for --color=always, 0 for --no-color. This matches pnpm. Without the flag Nub forces nothing either way, and the usual environment signals decide:
NO_COLOR=1 nub run -r build # off, whatever the terminal says
FORCE_COLOR=1 nub run -r build # on, even when the output is pipedSetting both is contradictory, and the two variables disagree about which should win: Nub honors NO_COLOR, Node honors FORCE_COLOR. On a prefixed run Nub applies its choice to the scripts as well, so the labels and the wrapped lines agree. A run that isn't prefixed passes both through untouched.
--node
By default, the nub executable is aliased as node for the duration of a nub run call, so any subprocess that spawns node (a script shebang, child_process.spawn("node", …)) gets Nub's augmentation too. Pass --node to turn that off for one invocation — see the runtime overview for the full contract.
nub run --node testWhat still happens under --node: script lookup, workspace walk-up, --filter evaluation, npm_* env injection, node_modules/.bin on PATH, and the pre / post lifecycle hooks. Use it when a script's #!/usr/bin/env node shebang chain expects plain Node, when bisecting a Nub bug, or when CI needs byte-exact Node runtime behavior with Nub's workspace selection.
Related
- Bin runner —
nubx, a local CLI or a fetched one, resolved in that order;nub execandnub dlxfor each half on its own. - Running files —
nub <file>for TypeScript and JavaScript execution. - Watch mode — restart on file changes.
- FAQ — monorepo, package-manager, and
--nodequestions answered briefly.
Watch modenub watch
Restart-on-change for files and scripts, driven by the resolved dependency graph plus your environment files, tsconfig, and package manifest — no glob list to maintain.
Bin runnernubx
Run a CLI by name — the project's own copy when it is installed, fetched from the registry when it is not, and never fetched silently in CI.