Varlock
First-party Varlock support — a project carrying an env-spec schema hands environment loading over entirely, so validation, providers, secret redaction, and type generation all happen on Varlock's own terms, in monorepos and from subdirectories included.
Nub has first-party support for Varlock. Add it to a project and Nub stops loading .env* files, handing your environment to Varlock instead — no wrapper command, no import in your source, no configuration.
nub add -D varlock # then write a .env.schema
nub server.ts # validated, with secrets redactedAn @env-spec schema — conventionally .env.schema — declares your variables with types and validation, using comments as the schema language:
# @required @type=port
PORT=3000
# @required @sensitive @type=string(startsWith=sk-)
API_KEY=The schema holds no secrets, so it is safe to commit. At runtime you get coercion, validation, and redaction on anything marked @sensitive.
How the handoff works
Nub does not resolve the schema, inject its values, or redact anything. It puts Varlock in front of Node and steps out of the way:
varlock run --path <the schema's directory> -- node server.tsThat indirection is the point. A schema is a graph, not a file list: it picks its own environment selector with @currentEnv, pulls values from providers, and can resolve differently depending on what you pass it. Only Varlock knows how to resolve it, so Nub does not try, and there is no second interpretation to drift out of sync.
Everything Varlock does on its own works unchanged, because it is running as itself:
APP_ENV=production nub server.ts # or whatever selector your schema declaresType generation, providers, validation, secret redaction — whatever your schema asks for happens, on Varlock's own terms. Nub adds nothing and takes nothing away.
When Nub hands over
Three things have to be true. Miss any one of them and Nub keeps loading .env* exactly as it always has.
- A
.env.schemain the project root, or in the workspace root above it - The file is actually
@env-spec— a# ---divider or a# @decoratorline - Varlock resolves, from
node_modules/.binup to the workspace root, thenPATH
The middle condition exists because the filename is contested. dotenv-extended has used .env.schema for its own incompatible format since 2016, so Nub recognizes the name without claiming it: a schema written for another tool never routes your run through Varlock, and never draws a warning either.
Monorepos and subdirectories
Nub looks for the schema in your project root, then the workspace root, and points Varlock at whichever it finds. A workspace member with no schema of its own uses the one at the root, and a member that ships its own uses that instead.
cd pkgs/web && nub dev.ts # resolves the schema at the workspace rootThis is also what makes running from a subdirectory work. Varlock on its own takes its entry point from the current directory, so cd src && varlock run -- node app.js reports no .env files found; Nub passes the directory it found the schema in.
Redaction
Varlock redacts a stream it is piping — console methods, raw process.stdout.write, and anything a subprocess prints. Attached to an interactive terminal it passes output through untouched, so a secret you print at a prompt is not redacted. That is Varlock's behavior either way; running under Nub neither adds redaction nor removes it.
Keeping Nub's own loading
The handoff displaces automatic .env* discovery only. Naming a file explicitly is a deliberate instruction, and it still wins:
nub --env-file=.env.ci server.ts # loads .env.ci; Varlock does not runThe same holds for an envFile in nub.jsonc. To turn the handoff off along with the rest of Nub's augmentation, use --node or NODE_COMPAT=1.
When Varlock is not installed
What Nub does depends on whether your project asked for Varlock, because the two cases mean different things.
Your package.json | What Nub does |
|---|---|
Declares varlock | Fails, and tells you to run nub install |
| Does not declare it | Loads .env* as usual, and warns once |
A declared dependency that will not resolve means a broken tree — a pruned --prod install, a partial node_modules. Falling back to .env* there would hand your program an environment it never asked for: no defaults, no validation, no providers, and for a schema-only project with no committed .env, nothing at all. Nub refuses instead. With nothing declared, the project may simply not know the file means anything to Nub, so Nub keeps going and says so.
Watch mode
Under nub watch, Varlock resolves once at watcher startup and Node's --watch supervisor re-execs your program inside it. Values are therefore fixed for the life of the watcher, where Nub's own .env* files are re-read on every restart. Editing a value means restarting the watcher.
Dependency install scripts
A dependency's postinstall or build script does not get your schema. It runs with its own package directory as the working directory, so Nub anchors on that package's package.json and never looks as far up as your project root — the same reason those scripts do not see your .env* files either.
Your own scripts are unaffected: nub run build runs at the project root, so the schema applies normally.
Environment files
Automatic environment-file loading — the file set, mode selection from APP_ENV or a clamped NODE_ENV fallback, precedence, variable expansion, the skip under the test environment, loading explicit files with --env-file, and disabling all loading with --no-env-file.
Loaders
Import JSON, JSONC, JSON5, TOML, YAML, and plain-text files as default exports, plus any file as raw text via an import attribute.