Run nub <verb> with a verb nub doesn't recognize and it resolves an executable named nub-<verb>, then runs it with the rest of the arguments forwarded untouched. This is the same convention behind git-foo and cargo-foo.

nub changeset publish      # runs nub-changeset publish
nub release --tag beta     # runs nub-release --tag beta

A plugin is just a package that installs a nub-<verb> bin — there is no plugin manifest, no registration step, and no config field to declare it. Install it like any other dependency and the verb works.

nub add -D nub-changeset   # ships a nub-changeset bin
nub changeset              # now resolves and runs it

Resolution

The prefixed name is resolved in two places, in order:

  • node_modules/.bin — the project-local bin, walking up from the current directory (the same chain nub run and nubx use).
  • PATH — a globally-installed plugin.

Only the prefixed nub-<verb> name is ever probed — never the bare verb. A mistyped command like nub bnuild resolves nothing and prints the usual not-a-command error; it can't fall through to a random bnuild binary on your PATH.

Built-ins always win

A plugin can never shadow a built-in verb. Native commands and package-manager verbs are matched before any plugin lookup, so nub run, nub install, nub add, and the rest always dispatch to nub itself, even if a nub-run bin exists in node_modules/.bin. The fallthrough fires only for a verb nub has no built-in for.

Script names take the same priority. A verb that names a script in the local package.json, or one of the conventional names dev, build, test, start, and lint, resolves to the nub run <verb> hint before the plugin lookup — so a nub-build or nub-test plugin is never reached through its bare verb. Name a plugin for something that isn't a script (nub-changeset, nub-release), or invoke a script-named one through nub run or by its bin directly.

Writing a plugin

A plugin is a runnable executable named nub-<verb> — a compiled JavaScript bin or a native binary, the same as any other node_modules/.bin entry or a git/cargo subcommand. There's no plugin-specific build target: author it in TypeScript and publish the compiled output, exactly as you'd ship any other bin. Installed bins aren't transpiled — the byte-parity boundary that keeps node_modules identical to plain Node — so the entry that actually runs has to be JavaScript or native.

A JavaScript plugin runs under nub's runtime augmentation, exactly as nub run runs a script — the version-gated globals are present. A native executable just runs. The plugin's exit code is what nub returns.

#!/usr/bin/env node
// nub-changeset — authored in TypeScript, published as compiled JS
const target = process.argv[2] ?? "staging";
console.log(`releasing ${target}`);

To run a plugin with augmentation disabled, pass --node before the verb — nub --node changeset, the same position a file run uses. Anything after the verb is forwarded to the plugin untouched, so a trailing nub changeset --node reaches the plugin as an argument and does not disable augmentation. See the runtime overview for the contract.

  • Package runnernubx, for running a tool by name without installing it as a subcommand.
  • Running scriptsnub run, which shares the bin-resolution chain.
  • Runtime — TypeScript-first execution and the augmentation a JS/TS plugin inherits.