Environment

The environment a script sees is built in four layers. Later layers win.

LayerSourceOverrides
1The parent process environmentNothing. It is the base
2envFile assignmentsNothing that is already set
3The script's env mapLayers 1 and 2, except reserved names
4RUNE_SCRIPT_NAME, RUNE_ROOT, RUNE_PACKAGE_DIREverything

PATH is rebuilt between layers 1 and 2, described below.

env

A map of literal values, applied last.

build: {
  command: 'vite build',
  env: { NODE_ENV: 'production' },
}

An env entry wins over the machine's value for the same variable. That is the difference between env and envFile: env is a decision made in the config, so it takes effect.

envFile

A dotenv file, read relative to the config that declared it.

test: {
  command: 'vitest run',
  envFile: '.env.test',
}

A file never overrides a variable that is already set. An assignment whose variable exists in the environment is skipped, and the skip is reported on stderr with the variable, the file and the reason:

warning: `DATABASE_URL` from `.env.test` was ignored: the process environment already sets it

This makes a one-off override work the way it looks like it should:

DATABASE_URL=postgres://localhost/scratch pnpm test

Without the rule, the file would silently win and the command above would do nothing.

A missing envFile is an error, not a warning. A test suite that runs against the wrong database because a file was renamed is worse than a failed run. The message names the script, the path the file resolved to, and the config that declared it. A line that cannot be parsed as an assignment is an error too, naming the file and the line number.

Across an extends chain

Every level of an extends chain contributes its own envFile, and each one fills gaps only. The files are consulted from the level nearest the script outward, so the nearer config wins a variable both of them set, and the outer file's assignment is reported as ignored — the same way a config narrowing what it inherits narrows its env map.

The RUNE_ prefix

Names beginning with RUNE_ are reserved. An attempt to set one from a file or from an env map is ignored and reported:

warning: `RUNE_ROOT` from this script's `env` was ignored: `RUNE_` is reserved for rune's own variables

Rune sets three of them for every script:

VariableValue
RUNE_SCRIPT_NAMEThe name passed to rune run
RUNE_ROOTThe directory holding the config that defined the script
RUNE_PACKAGE_DIRThe package directory the run started from

PATH

Rune prepends every node_modules/.bin directory from the package directory up to the config root, most specific first, using the platform separator.

Every level in between is included, not just the two ends. For a run in packages/api of a repository rooted at /repo:

/repo/packages/api/node_modules/.bin
/repo/packages/node_modules/.bin
/repo/node_modules/.bin
<the inherited PATH>

Order decides which copy of a tool wins. A package that pinned an older version of a tool gets its own copy, not the root's. Directories that do not exist are still added, matching npm, so a directory created later lands in the right position without a re-run.

This is what makes "command": "vitest run" work with no path and no npx.

Windows

Environment variable names are case-insensitive on Windows and the operating system enforces that, not the caller. Rune merges case-insensitively there, so a child never receives both PATH and Path. The name is preserved as the parent wrote it, so a machine that says Path hands the child Path.

Seeing the result

rune inspect prints what the config contributes, including the assignments that were ignored.

$ rune inspect test
test

command      vitest run
directory    packages/api
environment  NODE_ENV=test
ignored      `DATABASE_URL` from `.env.test` was ignored: the process environment already sets it

resolved through
  rune.config.ts  test  runs `vitest run`

PATH and the three RUNE_ variables are not in the delta. Rune sets them for every script, so listing them would say nothing about this one.