Reading variables

Wrap a Build

envless build [target] -- <cmd>: inject decrypted variables into a build command, nothing on disk.

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

bash
envless build -- vite build
envless build -- npm run build
envless build web -- next build

Everything 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

bash
envless build web -- vite build
envless build cloudflare -- wrangler deploy

The 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.

text
✔ 12 variables from Production → building web → vite

Anything 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

bash
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:

FlagEnvironment variable.envless keyWhat it does
--workspacenoneworkspaceWorkspace 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
--productENVLESS_PRODUCTproductDisambiguates a project slug that exists in more than one product. Optional when the slug is unique in the workspace
--projectENVLESS_PROJECTprojectProject slug or uniqueId. Always required
--envENVLESS_ENVenvironmentsEnvironment 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:

CategoryNames
Node and ElectronNODE_OPTIONS, NODE_PATH, NODE_REPL_EXTERNAL_MODULE, ELECTRON_RUN_AS_NODE
Dynamic linkerLD_PRELOAD, LD_LIBRARY_PATH, LD_AUDIT, DYLD_INSERT_LIBRARIES, DYLD_LIBRARY_PATH, DYLD_FRAMEWORK_PATH
Executable lookupPATH
BunBUN_INSPECT, BUN_INSPECT_CONNECT_TO
RubyRUBYOPT, RUBYLIB, BUNDLE_GEMFILE, GEM_HOME, GEM_PATH
PythonPYTHONPATH, PYTHONHOME, PYTHONSTARTUP, PYTHONUSERBASE
PHPPHPRC, PHP_INI_SCAN_DIR
Envless internalsENVLESS_CLIENT_ENV, ENVLESS_INJECTED

The warning names every value that was dropped:

text
⚠ [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_OPTIONS

envless 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:

json
{
    "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

CI / production

The same command works non-interactively. Swap the cached login for a machine key and provide the decryption key:

bash
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 build

The 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

MessageExitWhy
Nothing to build. Usage: envless build [target] -- <command>2No -- on the command line, or nothing after it
Not signed in. Run envless login or set ENVLESS_TOKEN.1No cached device session and no ENVLESS_TOKEN
Missing --workspace, --project, --env.1A 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.1The token is an ev_sk_ key and no decryption key was provided
<workspace> is locked. Run envless passphrase set, or set ENVLESS_KEY.1Device login with no cached key, no ENVLESS_KEY and no ENVLESS_PASSPHRASE
At most 20 environments can be loaded in one run.1More than 20 distinct environments in --env. Device login only
Could not load variables (...)1Device 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.1The workspace slug or uniqueId does not match one you belong to
Project <ref> not found in this workspace.1Wrong project slug, or a --product that does not hold it
Environment <slug> not found in this project.1Device login only. In machine mode the same typo fails at the fetch instead
Could not load variables for <env> (...)1Machine 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.1Wrong key or passphrase. One failure aborts everything, so no half decrypted build ever starts
Could not start <command>: <reason>127The binary is not on PATH, or is not executable
The child was killed by a signal128 + signal130 for SIGINT, 143 for SIGTERM
The child exited on its ownthe child's codeA failing build stays a failing build, so CI sees the real result

Notes

  • Variables are never written to disk. build does not read or write the on-disk cache that @goenvless/env keeps and envless cache clear wipes.
  • 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.
  • build reads but never writes: an ev_sk_ machine key is enough, and nothing in your workspace changes.
  • envless build --help prints the build section of the CLI help. A --help placed after the -- goes to the child, as any other flag does.