Reading variables

Wrap a Process

envless run -- <cmd>: inject decrypted variables into a single process, nothing on disk.

envless run decrypts your variables and launches a child process with them set in the environment. No files are written, no imports added, and the variables disappear when the process exits.

The envless command ships in @goenvless/cli. Install it with npm i -g @goenvless/cli@latest.

Basic usage

bash
envless run -- npm start
envless run -- node server.js
envless run -- python manage.py runserver

Anything after -- is the command to run. 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.

Picking an environment

bash
envless run --env production -- node server.js
envless run --env staging,development -- npm test

--env takes a comma separated list. Environments are merged left to right, so a name defined in development overwrites the same name from staging.

If you've linked multiple environments and don't pass --env, the first one in .envless is used (with a warning). The --env value also accepts the ENVLESS_ENV environment variable when the flag is absent.

--product (or ENVLESS_PRODUCT, or product in .envless) disambiguates a project slug that exists in more than one product. It is optional when the slug is unique within the workspace.

Envless values win over ambient

The child inherits the parent's full environment (HOME, PATH, and everything else pass straight through), but any variable Envless decrypts overrides a same-named ambient var. The merge is { ...process.env, ...safeValues }, where safeValues is the decrypted set minus the refused names below. Envless takes precedence.

Names are injected exactly as they are stored. run never upper-cases a name, so a variable saved as port arrives as port. Inside one Envless environment that is unambiguous, because no environment can hold two names that differ only in case. The merge itself is a plain object though, and so is a process environment on Linux and macOS, so merging two environments with --env staging,development can put both API_KEY and api_key into the child as two separate variables. Keep one casing per name across the environments of a project.

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.DATABASE_URL still returns the decrypted one. With run the decrypted value wins both ways, so a stale DATABASE_URL left in your shell can't shadow the real one.

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 run drops the value and prints a warning. Any ambient value survives untouched, so refusing a stored PATH cannot break the child.

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

The @goenvless/env runtime applies the same list when it populates process.env, so a variable refused by run is also refused there.

In package.json

Wrap your existing scripts so teammates don't have to think about it:

json
{
    "scripts": {
        "dev": "envless run -- next dev",
        "start": "envless run --env production -- next start",
        "test": "envless run --env test -- vitest"
    }
}

When to pick run over the other modes

Use run

You want one-shot injection, nothing persisted, works with any language.

Use runtime

Node and Bun apps where you want typed env.X from @goenvless/env/server and no CLI step, or Python apps with envless-env. Edge runtimes have no filesystem, so they use @goenvless/env/edge, whose load() returns a plain record instead.

Use sync

You need .env.<slug> files for Docker, Rails, python-dotenv or shell scripts.

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 run --project billing-api --env production -- node server.js

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.

What it does under the hood

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. run decrypts strictly: one value that fails to decrypt aborts the whole command.

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.

Forward signals and exit code

SIGINT / SIGTERM are forwarded to the child so Ctrl-C works. The parent exits with the child's code, or with 128 + signal when the child is killed by a signal.

Notes

  • Variables are never written to disk. run does not read or write the on-disk cache that envless cache clear manages.
  • The child sees them via standard process.env, except for the refused names above.
  • A variable with no value is skipped with Variable <name> has no value, skipping it.
  • Exit codes: 2 when no command follows --, 1 when the target, key, fetch, or decryption fails, 127 when the command cannot be started, and otherwise the child's own code.

Going deeper

The most common reason to reach for run rather than sync is that something else is reading your repository.