Script runner
Use nub run as a drop-in for npm run / pnpm run — lifecycle hooks, npm_* env vars, arg forwarding, and pnpm-style workspace filters, ~7× faster on the cold path.
The nub run command executes the scripts in your package.json. It is a drop-in replacement for npm run and pnpm run — ~7× faster on the cold path, with the full workspace and lifecycle-hook surface preserved. It reads your package.json directly; package management stays with whatever you already use (pnpm, npm, yarn, bun). This page is a cookbook: each section is a task with the exact command to run.
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. (If you type nub build and build is a real script, Nub's error tells you to type nub run build.) See Running files for how nub <file> works.
Forward arguments
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. Anything after the script name belongs to the script — full stop. The explicit separator still works for muscle memory:
nub run test -- --watch # identical result; the -- is optionalThe rule is positional and never ambiguous: a 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's interpretation is hooks-only — there is no install pipeline to skip.
nub run --ignore-scripts buildThe npm_* environment
Nub populates the child process with the full npm-compatible environment, so scripts and tooling that read npm_* variables behave exactly 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, and the full npm_config_* and npm_package_* enumerations, and INIT_CWD is set to the directory you invoked nub run from. Inside 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 Run without augmentation below.
--script-shell
If your .npmrc sets script-shell, Nub uses it to invoke script bodies. The rest of .npmrc (registry, auth, node-linker, hoist-pattern) is package-manager territory and is deliberately ignored.
# .npmrc
script-shell = /bin/bashYou can also override the shell per invocation:
nub run --script-shell /bin/bash build-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 AND its dependencies (downstream)
nub run --filter "...@org/web" build # @org/web AND everything that depends on it (upstream)
nub run --filter "@org/web^..." build # @org/web's dependencies only
nub run --filter "...^@org/web" build # @org/web's dependents onlyThese are exactly the forms pnpm --filter supports, so existing monorepo muscle memory carries over directly.
--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 the last commit
nub run --filter "...[origin/main]" test # changed packages AND their dependents--parallel, --sequential
Across a workspace, Nub runs packages concurrently up to a cap (CPU count by default) while respecting topological order. Tune or override that.
nub run -r --workspace-concurrency 4 build # cap at 4 concurrent scripts
nub run -r --parallel dev # all at once, no topo ordering or cap
nub run -r --sequential migrate # one at a time, ignore topologyUse --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 # run every package, report failures at the end--resume-from
When a long topological run dies partway through, --resume-from skips the topological predecessors of a package so you restart where it broke instead of from the top.
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 # run "lint" at the workspace root only
nub run -r --include-workspace-root lint # all members PLUS the rootNote that -w 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, per-package-prefixed output (<pkg> | <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 CI parsingAnd --silent (or -s) suppresses Nub's $ <command> preamble (it does not suppress the script's own stdout):
nub run --silent build--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 TypeScript transpilation, .env loading, and polyfills too. Pass --node to turn that augmentation off for one invocation.
nub run --node testWith --node, the PATH shim, the transpile/preload hook, and experimental-flag injection are all skipped — child node calls resolve to your real Node binary. What still happens: script lookup, workspace walk-up, --filter evaluation, npm_* env injection, node_modules/.bin on PATH, and the pre / post lifecycle hooks. Reach for it when a script's #!/usr/bin/env node shebang chain expects plain Node, when bisecting a Nub bug, or when CI wants byte-exact Node runtime behavior with Nub's workspace selection.
There is no NODE_COMPAT=1 environment variable — pass --node on each invocation that needs it.
vs. node --run
Node shipped its own built-in script runner in 22.0 (node --run). It is fast, but deliberately stripped down: no pre / post lifecycle hooks, no workspace-aware lookup, no npm_* package-manager env vars, no implicit arg forwarding. Real projects routinely hit one of those gaps. The goal for nub run is that speed without the compatibility cliff — same script lookup, same npm_* environment, same node_modules/.bin chain, same lifecycle hooks, plus the pnpm-style workspace filters above.
Related
- Running files —
nub <file>for TypeScript and JavaScript execution. - nubx — run locally installed CLIs from
node_modules/.bin. - 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 .env*, tsconfig.json, and package.json — no glob hygiene required.
Package runnernubx
Use nubx (or nub exec) to run the CLIs in your project's node_modules/.bin — a drop-in for npx and pnpm exec, an order of magnitude faster on cold start.