CLI exec

Run a command with your secrets in its environment, without writing a .env file.

Exec command

envilder exec resolves every secret in your map file and starts a command with those values in its environment. Nothing is written to disk: there is no .env file to gitignore, keep in sync or clean up. The map file and its $config work exactly as in pull mode.

Usage

envilder exec [options] [--] <command> [args...]

Where your command starts: the -- rule

Envilder options come first. Your command starts at the first argument that is not an Envilder option, or right after -- when present. Everything from there on is passed to your command verbatim, even if it looks like an Envilder option. The -- is optional; you only need it when the command itself starts with -.

# Everything belongs to npm
envilder exec npm run dev

# --map is Envilder's, the rest is npm's
envilder exec --map=cfg.json npm run dev

# --map=x belongs to npm: it comes after the command
envilder exec npm run dev --map=x

# Same as above, with an explicit separator
envilder exec --map=cfg.json -- npm run dev

Options

OptionDescription
--mapPath to the map file (default: envilder.json)
--profileAWS CLI profile. Overrides $config.profile
--provideraws or azure. Overrides $config.provider
--vault-urlAzure Key Vault URL. Overrides $config.vaultUrl

Envilder options go after exec, never before it. An option written before exec is rejected with exit code 125, and Envilder shows the corrected command:

$ envilder --map=config/prod.json exec -- npm start
error: Put envilder options after "exec": envilder exec --map=config/prod.json -- npm …

Exec mode never reads or writes a .env file and never pushes secrets, so --envfile, --push, --key, --value and --secret-path are rejected with exit code 125 before any provider call. An unknown option before your command is also an error, so a typo such as --mapp never reaches your command by accident.

OptionMessage
--envfile--envfile cannot be used with exec: exec mode never reads or writes a .env file.
--push--push cannot be used with exec.
--key, --value, --secret-pathSingle-secret push options cannot be used with exec.

Examples

# Start your dev server with the secrets from envilder.json
npx envilder exec -- npm run dev

# Use a specific map file
envilder exec --map=config/envilder.json -- node server.js

# Azure Key Vault via CLI flags
envilder exec --provider=azure   --vault-url=https://my-vault.vault.azure.net -- dotnet run

# Docker Compose can interpolate the secrets in compose.yaml
envilder exec -- docker compose up

Or wire it into your package.json scripts:

package.jsonjson
{
  "scripts": {
    "dev": "envilder exec -- vite",
    "start": "envilder exec --map=config/prod.json -- node server.js"
  }
}

Output

Your command owns stdout. All Envilder output goes to stderr, so envilder exec -- node print.js > out.json writes only your command's output to out.json. On success Envilder prints a single summary line that names your command by program name only, because arguments can carry secrets:

envilder: 2 secrets resolved (aws, profile=dev) → npm …

Common use cases

In every case the values exist only in the environment of your command and the processes it starts, Envilder never creates a .env file, and your command's exit code becomes Envilder's exit code. What your command does with the values is up to it: Docker, for example, keeps a container's environment in its on-disk metadata (visible with docker inspect) until the container is removed.

Start LocalStack with its auth token

When you need it: LocalStack needs LOCALSTACK_AUTH_TOKEN and you do not want the token in a .env file or in your shell history.

envilder.jsonjson
{
  "LOCALSTACK_AUTH_TOKEN": "/envilder/development/localstack/authToken"
}

With Docker Compose, reference the variable in compose.yaml. Compose takes its value from the environment it runs in, so no .env file is needed:

compose.yamlyaml
services:
  localstack:
    image: localstack/localstack:stable
    ports:
      - "4566:4566"
    environment:
      LOCALSTACK_AUTH_TOKEN: ${LOCALSTACK_AUTH_TOKEN}
envilder exec -- docker compose up

With plain Docker, -e NAME without a value copies the variable from the environment of the docker client, so the token never appears on the command line:

envilder exec -- docker run --rm -e LOCALSTACK_AUTH_TOKEN -p 4566:4566 localstack/localstack:stable

The same setup with Testcontainers (TypeScript, Python, .NET) and Aspire is in envilder-examples

Integration tests that start containers

When you need it: your tests start containers (for example, with Testcontainers) and read a token or credentials from the environment (process.env, os.environ, Environment.GetEnvironmentVariable) to pass them in.

envilder exec -- npm test
envilder exec -- dotnet test
envilder exec -- pytest

A failing test run keeps its exit code, so CI still fails. If a secret cannot be resolved, the tests do not start and the exit code is 125.

Database migrations and one-off scripts

When you need it: a migration tool or a script needs DATABASE_URL once, and you do not want the connection string on disk. Use --map to run the same command against another environment.

envilder.jsonjson
{
  "DATABASE_URL": "/my-app/development/database/url"
}
envilder exec -- npx prisma migrate deploy
envilder exec -- python scripts/backfill.py

Terraform and other IaC tools

When you need it: a Terraform input variable holds a secret. Terraform reads TF_VAR_<name> environment variables as values for the input variable <name>.

envilder.jsonjson
{
  "TF_VAR_db_password": "/my-app/development/database/password"
}
envilder exec -- terraform plan

Declare the variable with sensitive = true so Terraform hides it in plan output. Terraform still stores values it writes into resources in its state, so protect the state as usual.

Local dev server through package scripts

When you need it: everyone on the team should start the app with the same secrets by running npm run dev.

package.jsonjson
{
  "scripts": {
    "dev": "envilder exec -- next dev"
  }
}

Add envilder as a dev dependency (npm install --save-dev envilder) so the script finds it in node_modules/.bin.

A step in GitHub Actions

When you need it: one step of a workflow needs the secrets, and only that step. Configure AWS credentials with OIDC, then run your command through npx envilder exec:

permissions:
  id-token: write # Required for OIDC
  contents: read

steps:
  - uses: actions/checkout@v5
  - uses: aws-actions/configure-aws-credentials@v5
    with:
      role-to-assume: arn:aws:iam::123456789012:role/GitHubActionsRole
      aws-region: us-east-1
  - run: npx envilder exec -- npm test

Resolved values are registered with GitHub log masking before your command starts, and the next steps do not see them.

Several commands with the same secrets

When you need it: you want to chain commands or keep working in a shell that has the secrets.

envilder exec -- sh -c 'npm run migrate && exec npm start'
envilder exec -- bash

Quote the whole sh -c script in single quotes so your own shell does not run && outside envilder exec. The exec before the last command replaces the shell with it, so signals forwarded by Envilder reach npm start directly. In the interactive shell every command you run sees the secrets; type exit to leave it.

Behavior

Exit codes

Exit codes follow GNU env and docker run, so you can tell an Envilder failure apart from your command's own exit code:

CodeMeaning
nYour command exited with code n
128 + nYour command was killed by signal n (for example, 137 for SIGKILL)
125Envilder failed before starting the command: bad options, missing command, no map file, unresolved secrets or expired credentials
126The command exists but the operating system refused to run it (for example, Permission denied)
127The command was not found

Pull and push modes keep exiting with 1 on failure.

Fail-closed

Pull mode warns and skips a missing secret. Exec mode never starts your command with an incomplete set of secrets: if any secret is missing, empty or cannot be read (for example, access denied), the command is not started, Envilder lists the affected variables with their masked paths and exits with 125. Values are resolved once, when the command starts.

Environment

Your command inherits the full environment of the shell that runs Envilder, plus every variable in the map file. The map file wins: when an inherited variable has the same name as a mapped one, your command sees the value from the provider, and Envilder prints the names (never the values) on stderr:

Overriding inherited variables: API_KEY

Envilder does not start an extra shell: arguments are passed to your command as a list. Your own shell still parses the command line first, so quote what it should not interpret: '$DB_URL' or '&&' in single quotes reach your command literally, while an unquoted $DB_URL expands to your shell's value (not the resolved secret) and an unquoted && runs a second command outside envilder exec. If you need shell features with the secrets, start a shell explicitly:

envilder exec -- sh -c 'echo $DB_URL'

GitHub Actions masking

When GITHUB_ACTIONS=true, Envilder registers every resolved value with GitHub log masking (::add-mask::), so values your command prints are masked in the job log. Every registration is written to stderr before your command starts; if one cannot be written, the command does not start (exit code 125). The Envilder GitHub Action does not offer exec mode: run the CLI from a run step.

- run: npx envilder exec -- npm test

Do not redirect the stderr of envilder exec away from the step log (for example, with 2> file or 2>/dev/null): that is where the runner reads the masks from. The runner reads stdout and stderr through separate pipes, so a value your command prints immediately can, in rare cases, reach the log before its mask is processed.

Signals

stdin, stdout and stderr are inherited and the TTY is preserved, so REPLs, shells and prompts work as usual. Ctrl+C in a terminal reaches your command directly. SIGTERM and SIGHUP (for example, from docker stop or Kubernetes) are forwarded to your command, and Envilder waits for it to exit, with no timeout of its own, before returning its exit code. Signals go to the direct child only: a launcher such as npm is responsible for stopping its own children.

⚠️
Not a sandbox. Exec mode never creates a .env file, and it leaves any existing one untouched: if you are moving from pull, delete your old .env yourself. It does not isolate the process. Your command and every process it starts can read the secrets in their environment, including any tool or AI coding agent that runs env or printenv through it. On Linux, other processes running as the same user can read /proc/<pid>/environ. Only run commands you trust with the secrets you map.
💡
Runnable examples. Start LocalStack with envilder exec docker run, with the auth token resolved from AWS SSM without a .env file (Docker keeps it in the container metadata until the container is removed), or walk through push, pull and exec with npx envilder: envilder-examples · cli

Full exec command guide on GitHub: Windows details, SSO recovery and every error message