Usage
envless push
envless push --env production
envless push --env staging,production --prune
envless push --file ./config/prod.env --env production --yespush is the opposite direction of envless sync. It reads a local .env file, compares it against what the environment already holds, encrypts the changed values on your machine, and applies the whole change set in one transactional write per environment.
Nothing is uploaded in plaintext. Values are encrypted with the workspace key before the request leaves your machine, exactly as the dashboard does it, so the API only ever stores ciphertext.
envless is the binary from @goenvless/cli, a binary only package with nothing importable. Push is a command you run, never something the @goenvless/env runtime or an SDK client calls for you.
$ envless push --env production
┌ envless push
│
✔ production ← .env.production (12 variables)
⚠ Skipped 1 ENVLESS_ name in .env.production: ENVLESS_VERSION_LINK. Those boot envless itself, not your app.
✔ production 2 added, 1 changed, 0 removed, 1 untouched
› 1 remote variable missing from the file stayed in place. Pass --prune to delete them.
│
└ ✓ 2 added, 1 changed, 0 removed in 1 environment.There are no subcommands and no positional arguments. Anything you pass that is not a flag is ignored.
Which file gets pushed
Without --file, push looks for a file per environment, in this order:
.env.<envSlug>in the current directory, the same nameenvless syncwrites..envin the current directory.
If neither exists, that environment fails and push tells you to pass a path:
$ envless push --env production
┌ envless push
│
✖ No .env.production or .env here for production. Pass --file <path>.
│
└ ! 0/1 environment pushed, 1 failed.With --file, that exact path is used for every environment named in the run. The file actually read is printed on the environment line as production ← .env.production, so a multi-environment push always shows which file went where.
--file points at one file, so envless push --file .env.production --env staging,production pushes the same values to both. Leave --file off when you want the per-environment .env.<slug> files instead.
What the diff does
Push never blindly overwrites. For each environment it fetches the current variables, decrypts them locally, and builds a plan:
| Bucket | Meaning |
|---|---|
add | In the file, not in the environment. |
change | In both, but the decrypted value differs from the file. |
remove | In the environment, not in the file. Only when --prune is passed. |
untouched | In the environment, not in the file, and left alone because --prune was not passed. |
A name present in both with an identical value is not sent at all, so re-running push after a successful push is a no-op and prints already up to date.
Names are lined up without regard to case. A file line api_key=... matches a remote API_KEY and lands in change, not in add, so a push can never leave you with two variables that differ only in case. Envless keeps the casing the variable already has, and change writes the value alone, so a differently cased line never renames anything. For the same reason a file that defines both API_KEY and api_key cannot become two variables: an environment holds only one of them.
Because the plan is built from decrypted remote values, push needs the workspace key even for a comparison. See Passphrase.
Pruning
By default a variable that exists in the environment but not in the file stays where it is, and push tells you how many it left behind:
› 3 remote variables missing from the file stayed in place. Pass --prune to delete them.Pass --prune to make the file authoritative and delete those variables. Push asks once, before anything is written, naming the count per environment:
$ envless push --env staging,production --prune
┌ envless push
│
✔ staging ← .env.staging (9 variables)
✔ production ← .env.production (12 variables)
│
◆ Remove 3 variables from staging (1), production (2)?
│ ○ Yes / ● No
│
└ Aborted.The default answer is No. Declining writes nothing and exits 1. A push whose plan contains zero deletions never prompts, even with --prune passed.
--yes (or -y) skips the confirmation. Without a TTY and without --yes, push refuses with Refusing to run unattended. Pass --yes to confirm. and exits 1, so an unattended job can never quietly delete variables.
Dry run
--dry-run prints the plan and stops. It reads the file and the environment, but returns before any encryption, any confirmation and any write.
$ envless push --env staging,production --prune --dry-run
┌ envless push
│
✔ staging ← .env.staging (9 variables)
✔ production ← .env.production (12 variables)
staging .env.staging
add 1 STRIPE_WEBHOOK_SECRET
change 0
remove 2 OLD_REDIS_URL, LEGACY_FLAG
production .env.production
already up to date
│
└ ✓ Dry run, nothing written. 1 to add, 0 to change, 2 to remove.Only names are printed, never values, so a dry run is safe to paste into a pull request or a CI log. Note that remove is empty unless you also pass --prune, since without it there is nothing to delete.
Without --prune a fourth row, untouched, names the remote variables missing from the file that would be left in place. It prints whenever that bucket is not empty, including for an environment that is otherwise already up to date:
preview .env.preview
already up to date
untouched 2 OLD_SMTP_HOST, DEBUG_TOKENA clean dry run exits 0. If any environment could not be planned, the dry run reports it and exits 1 instead:
└ ! Dry run, nothing written. 1 environment could not be planned.Multiple environments
--env takes a comma-separated list of slugs or unique IDs. Resolution order is --env, then ENVLESS_ENV, then the environments array in .envless. On a TTY with none of those set, push opens a multi-select picker, or takes the only environment when the project has just one.
envless push --env staging,production
ENVLESS_ENV=staging,production envless pushEach environment is planned and applied independently. One bad environment does not roll back a good one, so a partial run reports what got through and exits 1:
└ ! 1/2 environments pushed, 1 failed.Duplicate entries in --env are collapsed, so --env production,production pushes once.
Names that get skipped
Two kinds of names in the file are dropped with a warning rather than pushed:
⚠ Skipped 1 ENVLESS_ name in .env.production: ENVLESS_VERSION_LINK. Those boot envless itself, not your app.
⚠ Skipped 2 unusable names in .env.staging: MY-VAR, 2FA_SECRET. Names take letters, numbers and underscores, and cannot start with a number.Anything starting with ENVLESS_ is bootstrap configuration for envless itself, read by the CLI, the @goenvless/env runtime and the @goenvless/sdk, envless-sdk and envless/sdk clients, so storing it as a project variable would be circular. Anything that is not letters, numbers and underscores starting with a letter or an underscore is rejected the same way the dashboard rejects it, which is the ^[A-Za-z_][A-Za-z0-9_]*$ shape every Envless write path enforces. Case is not part of that rule: port is a perfectly valid name and is stored as port.
Skipped names are not counted in the variable total on the environment line. They are also invisible to the diff, so with --prune a remote variable whose only local line was skipped counts as missing from the file and gets removed.
A third kind of name is pushed and stored, but never reaches a process environment. NODE_OPTIONS, NODE_PATH, NODE_REPL_EXTERNAL_MODULE, ELECTRON_RUN_AS_NODE, LD_PRELOAD, LD_LIBRARY_PATH, LD_AUDIT, DYLD_INSERT_LIBRARIES, DYLD_LIBRARY_PATH, DYLD_FRAMEWORK_PATH, PATH, BUN_INSPECT, BUN_INSPECT_CONNECT_TO, RUBYOPT, RUBYLIB, BUNDLE_GEMFILE, GEM_HOME, GEM_PATH, PYTHONPATH, PYTHONHOME, PYTHONSTARTUP, PYTHONUSERBASE, PHPRC and PHP_INI_SCAN_DIR change how a process runs code, so envless run and the @goenvless/env runtime decrypt them and then refuse to write them, with a warning naming each one. Push accepts them, so the value is stored and nothing will ever read it back from process.env. The same list also refuses ENVLESS_CLIENT_ENV and ENVLESS_INJECTED, which push already drops with the other ENVLESS_ names.
File format accepted
Push uses the same parser the rest of the CLI uses, so a hand-written file works as well as a generated one:
- Blank lines and
#comment lines are ignored. - A leading
exportis stripped, so a sourceable file pushes cleanly. - Values may be unquoted, single quoted or double quoted. Quoted values may span multiple lines.
- For unquoted values, a
#starts a trailing comment and is trimmed off. A#that is not preceded by a space is kept, soCOLOR=#fffsurvives. - For quoted values, anything after the closing quote is discarded, so
KEY="value" # notestoresvalue. - Inside double quotes,
\n,\r,\\and\"are unescaped. Single quoted values are taken literally. - The generated header that
envless syncwrites is stripped, so a sync followed by a push does not create a variable out of the header. - If a name appears twice, the last occurrence wins.
- Parse errors report the line number as it appears in the file, header included.
A file the parser cannot read fails that environment and nothing is written for it:
✖ Could not read .env.production for production (Line 14 is not a NAME=value pair.).
✖ Could not read .env.staging for staging (Unterminated quote starting on line 9.).Drift detection
Every push sends a full baseline of the environment: each variable's unique ID and its last-modified timestamp as push saw it. The API takes a lock on the environment, compares that baseline against what is live, and refuses the entire change set if anything was added, removed or modified in between.
$ envless push --env production
┌ envless push
│
✔ production ← .env.production (12 variables)
✖ production was left as it was. DATABASE_URL changed while you were editing. Reload to pick up the latest values, then apply again.
› Run envless sync first, then push again.
│
└ ! 0/1 environment pushed, 1 failed.The write is a single transaction, so a refused push changes nothing at all. There is no partial apply inside an environment. Run envless sync to pick up the newer values, reconcile them in the file, then push again.
After a successful environment push, the CLI clears the ~/.envless/cache entry for that product, project and environment. That cache belongs to the @goenvless/env runtime, which writes it at boot and reuses it for 15 minutes before fetching again, so clearing it means the next boot on this machine reads what you just pushed instead of replaying the ciphertext it had already cached. The CLI only ever clears that cache, it never fills it, and envless run fetches every time either way. A process that is already up skips the window with load({ revalidate: true }), and you can clear the entry by hand with envless cache clear. See Caching & Reloads.
Verification code
The apply behind a push is a step-up guarded action: unless you verified a code for this workspace in the last hour, the server refuses it with auth.step_up_required, and push asks for the code before pushing the same ciphertext again. This happens after the diff and after any confirmation, so an aborted push never sends a code.
$ envless push --env production
┌ envless push
│
✔ production ← .env.production (12 variables)
› Enter the code we emailed you.
◆ Verification code
│ K7M2PX
└
› ✓ Verified. We will not ask again for an hour.
✔ production 1 added, 2 changed, 0 removed
│
└ ✓ 1 added, 2 changed, 0 removed in 1 environment.One code covers every environment in the same run and the rest of the hour. With two factor authentication on, the prompt takes an authenticator code or a recovery code as well as the emailed one.
--yes does not skip the code, only the confirmation. Without a TTY, push fails for that environment with This action needs a verification code. Run it from an interactive terminal, or do it from the dashboard. and leaves it as it was. A pipeline that pushes with a CLI token needs the workspace's Verification Codes setting turned off in the dashboard, see Workspace, which is a decision to make with your eyes open: it removes the code from every sensitive action in that workspace, not just from push.
Limits
One push may carry at most 1000 changes per environment, counting adds, changes and removes together. An environment that already holds more than 1000 variables cannot be pushed at all.
✖ production needs 1204 changes, over the 1000 allowed in one push.
✖ production holds 1350 variables, over the 1000 this command can push at once.Your plan's variable limit is enforced server side on top of that. If the applied change set would take the environment past it, the whole push for that environment is refused and the error names the allowed count.
Flags
| Flag | Description |
|---|---|
--file <path> | Push this exact file instead of looking for .env.<slug> then .env. Used for every environment in the run. |
--env <slugs> | Environment slug or unique ID, comma-separated for several. Falls back to ENVLESS_ENV, then .envless, then a multi-select picker on a TTY. |
--prune | Delete variables that exist in the environment but not in the file. Prompts once before writing. |
--dry-run | Print the plan per environment and exit without encrypting, confirming or writing. |
--yes, -y | Skip the prune confirmation. Required for --prune without a TTY. |
--workspace <ref> | Workspace slug or unique ID. Falls back to .envless, then to the default set by envless workspace use. |
--product <ref> | Product slug or unique ID. Falls back to ENVLESS_PRODUCT, then .envless. Only needed when the project slug exists in more than one product. |
--project <ref> | Project slug or unique ID. Falls back to ENVLESS_PROJECT, then .envless, then a picker on a TTY. |
Requirements
Push writes, so it needs more than a read credential:
| Requirement | How to satisfy it |
|---|---|
| A signed-in user token | envless login. An ev_sk_ machine key is rejected. |
| A workspace | --workspace, a .envless file, or the default set by envless workspace use. |
| A project | --project, ENVLESS_PROJECT, .envless, or the picker on a TTY. |
| At least one environment | --env, ENVLESS_ENV, .envless, or the picker on a TTY. |
| An unlocked workspace key | envless passphrase set, or ENVLESS_KEY / ENVLESS_PASSPHRASE. |
Machine keys are deliberately read only. ✗ Machine keys are read only. Run envless login to write from this machine. means ENVLESS_TOKEN holds an ev_sk_ key. Machine keys stay useful for envless sync and the runtime, see Keys.
Failures
Everything that goes wrong before the first write is reported and exits 1, apart from a value flag passed with no value, which exits 2. These messages print before the envless push banner, since they happen while the target is still being resolved:
| Situation | Message |
|---|---|
| A value flag with no value | --env needs a value. |
| Not signed in | Not signed in. Run envless login first. |
Machine key in ENVLESS_TOKEN | Machine keys are read only. Run envless login to write from this machine. |
| No workspace anywhere | Missing --workspace. Pass it inline, run envless link, or pick a default with envless workspace use <slug|id>. |
| Workspace key not cached | <workspace> is locked. Run envless passphrase set, or set ENVLESS_KEY. |
| No project, no TTY | Missing --project. Pass it inline or run envless link first. |
| Project or product not found | Names the identifier that did not resolve. |
Environment resolution happens once, before any environment is read, so these two abort the whole run rather than skipping one environment:
| Situation | Message |
|---|---|
| No environment, no TTY | Missing --env. Pass it inline or run envless link first. |
| Environment slug not in the project | Environment <slug> not found in this project. |
Past that point failures are per environment and the run continues with the rest:
| Situation | Message |
|---|---|
| No file to read | No .env.<slug> or .env here for <env>. Pass --file <path>. |
| File will not parse | Could not read <file> for <env> (<reason>). |
| Fetch failed | Could not load variables for <env> (<reason>). |
| A remote value will not decrypt | Could not decrypt <name> in <env>. Check your passphrase or ENVLESS_KEY. |
| Too many changes or too large an environment | Names the count and the 1000 limit. |
| Baseline drifted, or the plan was rejected | <env> was left as it was. plus the API reason, then Run envless sync first, then push again. |
Exit codes
| Code | When |
|---|---|
0 | Every environment applied, or every environment was already up to date, or a dry run planned cleanly. |
1 | Any environment failed, the target could not be resolved, the prune confirmation was declined or refused, or a dry run hit a planning failure. |
2 | A value flag was passed with no value, checked before anything is read. |
Environments that applied before a later one failed stay applied. The summary line tells you how many of each:
└ ✓ 4 added, 2 changed, 1 removed in 2 environments.
└ ✓ Already up to date.
└ ! 1/2 environments pushed, 1 failed.Going deeper
Before you push a .env you have been carrying around, it is worth checking what is in it.