envless build decrypts your variables and launches a build command with them set in the environment. It is envless run with a label on it: same resolution, same injection, same exit code, plus an optional target name so your logs say which build the variables went into. No files are written, no imports added, and the values disappear when the command exits.
The envless command ships in @goenvless/cli. Install it with npm i -g @goenvless/cli@latest.
Basic usage
envless build -- vite build
envless build -- npm run build
envless build web -- next buildEverything after -- is the command and its arguments. Envless resolves the target, decrypts the bundle, spawns the command with the values in its environment, forwards SIGINT and SIGTERM, and exits with the child's exit code.
The -- separator is required. envless build vite build has nothing after a --, so it stops with Nothing to build and exit code 2 before touching the API. envless run tolerates the missing separator, build does not.
The target label
envless build web -- vite build
envless build cloudflare -- wrangler deployThe optional positional between build and -- is a label and nothing else. It appears in the spinner and in the success line, and it changes nothing about what gets injected. It does not select a product, a project, or an environment, so use --env for that. If several positionals appear before --, the first is the label and the rest are ignored.
✔ 12 variables from Production → building web → viteAnything after -- belongs to the child, flags included, so envless build -- vite build --mode production hands --mode production to vite untouched. Every Envless flag goes before the --.
Picking an environment
envless build --env production -- vite build
envless build web --env staging,production -- npm run build--env takes a comma separated list. Environments are merged left to right, so a name defined in production overwrites the same name from staging.
When neither --env nor ENVLESS_ENV is set and .envless lists more than one environment, build warns and takes the first: Multiple environments linked, using development. Pass --env to choose. A list you pass explicitly is never truncated that way.
Every flag
build has four flags of its own. Each one resolves flag first, then the environment variable, then the .envless file:
| Flag | Environment variable | .envless key | What it does |
|---|---|---|---|
--workspace | none | workspace | Workspace slug or uniqueId. Required for a device login. Ignored when ENVLESS_TOKEN is an ev_sk_ machine key, since the key already scopes the workspace |
--product | ENVLESS_PRODUCT | product | Disambiguates a project slug that exists in more than one product. Optional when the slug is unique in the workspace |
--project | ENVLESS_PROJECT | project | Project slug or uniqueId. Always required |
--env | ENVLESS_ENV | environments | Environment slugs to decrypt and merge, comma separated. Always required |
If --workspace (device login only), --project or --env is still missing after all three sources, the command names the ones it is missing and exits 1 without contacting the API.
Credentials never come from a flag. ENVLESS_TOKEN carries the token, then ENVLESS_KEY or ENVLESS_PASSPHRASE carries the decryption key. A .env file in the working directory can supply the ENVLESS_* names too, except ENVLESS_TOKEN, ENVLESS_KEY and ENVLESS_PASSPHRASE, which the CLI refuses to read from a file, and a value already set in your shell always wins over the file.
The environment the child receives
The child inherits the parent's full environment (HOME, PATH, and everything else pass straight through), then the decrypted values are layered on top, so an Envless value overrides a same-named ambient one. The merge is { ...process.env, ...safeValues }, where safeValues is the decrypted set minus the refused names below. A config file that reads process.env at build time, like vite.config.ts or next.config.js, sees the values with no change to the config.
This is the opposite of the in-process loader (import { env }) for raw process.env reads only: that loader leaves an existing process.env entry alone, so an ambient value wins there, while env.API_URL still returns the decrypted one. With build the decrypted value wins both ways, so a stale API_URL left in the runner's shell can't shadow the real one.
Injecting a value into the build process is not the same as shipping it. Whether a name ends up inside your bundle is decided by your bundler's inlining rules, and anything inlined into a client chunk is public. See Server vs client and the framework pages for where that line sits.
Names that are never injected
A decrypted variable whose name changes how a process or its children execute code is never placed in the child environment, because storing one would turn write access to a variable into code execution. envless build drops the value and prints a warning. Any ambient value survives untouched, so refusing a stored PATH cannot break the build.
Twenty-six names are refused:
| Category | Names |
|---|---|
| Node and Electron | NODE_OPTIONS, NODE_PATH, NODE_REPL_EXTERNAL_MODULE, ELECTRON_RUN_AS_NODE |
| Dynamic linker | LD_PRELOAD, LD_LIBRARY_PATH, LD_AUDIT, DYLD_INSERT_LIBRARIES, DYLD_LIBRARY_PATH, DYLD_FRAMEWORK_PATH |
| Executable lookup | PATH |
| Bun | BUN_INSPECT, BUN_INSPECT_CONNECT_TO |
| Ruby | RUBYOPT, RUBYLIB, BUNDLE_GEMFILE, GEM_HOME, GEM_PATH |
| Python | PYTHONPATH, PYTHONHOME, PYTHONSTARTUP, PYTHONUSERBASE |
| PHP | PHPRC, PHP_INI_SCAN_DIR |
| Envless internals | ENVLESS_CLIENT_ENV, ENVLESS_INJECTED |
The warning names every value that was dropped:
⚠ [envless] These variables were decrypted but not written into the process environment, because their names change how this process or its children execute code, so storing one turns write access to a variable into code execution: NODE_OPTIONSenvless run and the @goenvless/env runtime apply the same list, so a variable refused here is refused there too. There is no flag to override it.
In package.json
Wrap the build script so nobody has to remember the prefix:
{
"scripts": {
"build": "envless build -- vite build",
"build:staging": "envless build web --env staging -- vite build",
"deploy": "envless build cloudflare --env production -- wrangler deploy"
}
}When to pick build over the other modes
Use build
A build or deploy step that needs variables at build time, and a label in the logs saying which build it was.
Use run
A long lived process: a dev server, an app server, a test run. Identical injection, no label.
Use sync
A build tool that insists on reading .env.<slug> files off disk instead of the environment.
CI / production
The same command works non-interactively. Swap the cached login for a machine key and provide the decryption key:
export ENVLESS_TOKEN=ev_sk_xxxxxxxxxxxx # machine key, dashboard → Keys
export ENVLESS_KEY=xxxxxxxxxxxx # workspace key, Settings → CI / Machine Access
envless build web --project billing-api --env production -- npm run buildThe ev_sk_ token scopes the workspace, so --workspace isn't required. You only need --project and --env (or a .envless file), and both also read ENVLESS_PROJECT and ENVLESS_ENV. Use ENVLESS_PASSPHRASE instead of ENVLESS_KEY if you'd rather derive the key from the passphrase. See CI/CD & Production for full setups.
Machine mode needs a decryption key up front: with an ev_sk_ token and neither ENVLESS_KEY nor ENVLESS_PASSPHRASE set, build stops before any fetch. It also skips the environment lookup and sends your slug straight to the API, so a mistyped environment surfaces as a failed fetch rather than Environment ... not found.
What it does under the hood
Split the command line
Tokens after the first -- are the child command and its arguments. What is left before it is the optional label. No -- means nothing to build, and the command stops there.
Resolve the target
Flags win, then ENVLESS_PRODUCT / ENVLESS_PROJECT / ENVLESS_ENV, then .envless. --workspace has no environment variable and comes from the flag or the file, and it is not required when ENVLESS_TOKEN is an ev_sk_ machine key.
Fetch + decrypt
ENVLESS_KEY is used when set, otherwise ENVLESS_PASSPHRASE, otherwise the workspace key cached by envless passphrase set. The bundle is fetched and decrypted in memory. build decrypts strictly: one value that fails to decrypt aborts the whole command, and nothing is spawned.
Spawn the child with env merged
The parent's process.env passes through, then the decrypted values are layered on top, minus the refused names. The child is spawned with inherited stdio, so the build's own output is untouched.
Forward signals and exit code
SIGINT / SIGTERM are forwarded to the child so Ctrl-C cancels the build. The parent exits with the child's code, or with 128 + signal when the child is killed by a signal.
Errors
| Message | Exit | Why |
|---|---|---|
Nothing to build. Usage: envless build [target] -- <command> | 2 | No -- on the command line, or nothing after it |
Not signed in. Run envless login or set ENVLESS_TOKEN. | 1 | No cached device session and no ENVLESS_TOKEN |
Missing --workspace, --project, --env. | 1 | A required field was absent from the flag, the environment variable and .envless. Only the missing ones are named |
Set ENVLESS_KEY (a workspace key) or ENVLESS_PASSPHRASE to decrypt in machine mode. | 1 | The token is an ev_sk_ key and no decryption key was provided |
<workspace> is locked. Run envless passphrase set, or set ENVLESS_KEY. | 1 | Device login with no cached key, no ENVLESS_KEY and no ENVLESS_PASSPHRASE |
At most 20 environments can be loaded in one run. | 1 | More than 20 distinct environments in --env. Device login only |
Could not load variables (...) | 1 | Device login only. The one request that loads the workspace, project, environments and variables failed: network, permissions, or a project slug that lives in more than one product and needs --product |
Workspace <ref> not found. | 1 | The workspace slug or uniqueId does not match one you belong to |
Project <ref> not found in this workspace. | 1 | Wrong project slug, or a --product that does not hold it |
Environment <slug> not found in this project. | 1 | Device login only. In machine mode the same typo fails at the fetch instead |
Could not load variables for <env> (...) | 1 | Machine mode only. The variable fetch failed: network, permissions, or a slug the API rejects |
Could not decrypt <name> in <env>. Check your passphrase or ENVLESS_KEY. | 1 | Wrong key or passphrase. One failure aborts everything, so no half decrypted build ever starts |
Could not start <command>: <reason> | 127 | The binary is not on PATH, or is not executable |
| The child was killed by a signal | 128 + signal | 130 for SIGINT, 143 for SIGTERM |
| The child exited on its own | the child's code | A failing build stays a failing build, so CI sees the real result |
Notes
- Variables are never written to disk.
builddoes not read or write the on-disk cache that@goenvless/envkeeps andenvless cache clearwipes. - The child sees the values through standard
process.env, except for the refused names above. - A variable with no value is skipped with
Variable <name> has no value, skipping it., and the build still runs. buildreads but never writes: anev_sk_machine key is enough, and nothing in your workspace changes.envless build --helpprints the build section of the CLI help. A--helpplaced after the--goes to the child, as any other flag does.