Usage
envless version publish [note...]
envless version list
envless version get <version>
envless version restore <version>
envless publish [note...]A version is a snapshot of every variable in one environment, stored under a label like v13. <version> is that label or the version's uniqueId. envless publish is a shortcut for envless version publish, so the first and the last line above are the same command.
The snapshot keeps the values exactly as the API holds them, still encrypted, and both the snapshot and the copy back happen server side. So publish, list, restore and a plain get all work on a locked workspace and never ask for a passphrase. Only get --reveal decrypts, and that one needs the key. See Passphrase.
Publishing is on every plan, and every publish is stored. What the plan decides is how much history you can see: on Free the workspace keeps only the latest published version, so list prints one row, get and restore know only that label, and restore itself is refused with Rollback needs a paid plan. Upgrade to restore an earlier version. Older versions and rollback need Dev or above; on Free the API answers 402 plan.upgrade_required for anything past the latest. Nothing is deleted on a downgrade, so the full history is back the moment the plan is. See Plans and billing.
Running envless version with no subcommand prints the usage and exits 0. An unrecognised subcommand prints ✗ Unknown subcommand: <word>, then the usage, and exits 2.
$ envless version
envless version
Publish a snapshot of an environment, then list, inspect, and restore it.
USAGE
envless version publish [note...] Snapshot an environment and notify the team
envless version list List the versions in an environment
envless version get <version> Show a version and the variables it holds
envless version restore <version> Replace the current variables with a version
<version> is a label such as v12, or a version id. Aliases: ls, rollback.
envless publish is a shortcut for envless version publish.
OPTIONS
--note <text> Note to store on the version, instead of positional words
--env a,b Environments to work in, only publish takes more than one
--project <p> Project to work in, defaults to .envless
--product <pr> Product, when a project slug is ambiguous
--workspace <w> Workspace to work in, defaults to .envless
--reveal Decrypt and show the values in envless version get
--json Print the list or the version as JSON
--yes, -y Skip the restore confirmation
EXAMPLES
envless publish Ship the checkout rewrite
envless version publish --note "before the migration" --env staging,production
envless version list --env production
envless version get v12 --env production
envless version get v12 --reveal
envless version restore v11 --env production --yes
A note is optional and stops at 200 characters.
A version keeps the values encrypted as they were, so publish and restore work on a
locked workspace. get masks values unless you add --reveal, which needs the key.envless version manages snapshots of your variables. The CLI's own version number is the flag envless --version (or -v), which touches nothing in your project. The envless binary itself ships in @goenvless/cli, a binary only package with nothing importable, so nothing on this page is reachable from @goenvless/env or @goenvless/sdk.
Subcommands
| Subcommand | Also accepted | What it does |
|---|---|---|
publish | Snapshot one or more environments and email the rest of the team. | |
list | ls | Print the versions in one environment as a table, newest first. |
get | Show one version and the names of the variables it holds. | |
restore | rollback | Replace the variables in one environment with a version. |
Publishing
envless publish
envless publish Before the Stripe migration
envless version publish --note "Before the Stripe migration" --env staging,productionPublish takes the environment as it stands and records every variable in it, values, type, visibility, secrecy, required flag and stored default. The label is generated server side by counting the versions the environment already holds and adding one, so the first publish is v1 and labels never repeat inside an environment.
$ envless publish Before the Stripe migration
┌ envless version publish
Before the Stripe migration
✔ Staging → v8 with 14 variables
✔ Production → v13 with 14 variables
│
└ ✓ Published in 2 environments. The rest of the team was emailed.envless publish is the same code with the same banner, so a transcript always says envless version publish even when you typed the shortcut.
Nothing local changes. Publish does not rewrite your .env files and does not clear the on-disk cache, because no variable moved.
Everyone else in the workspace is emailed the label, the environment, the variable count and the note. You are never emailed your own publish. On a private product only the members who have access to that product are told.
An environment holds at most 1000 versions on every plan. Past that the publish is refused with the API message:
✗ Could not publish Production (This environment has reached its limit of 1000 versions.).A successful publish fires the variable_version.created workspace webhook.
The note
The note is optional. Every word after publish becomes the note, joined with a single space, so quoting is optional:
envless publish Before the Stripe migration
envless version publish Cut for the release branch --env staging--note wins whenever it still has content after trimming, otherwise the positional words are used. With neither, the version is stored without a note and the list shows - for it.
A note is at most 200 characters. The check runs before any network call, so a long one costs nothing and exits 2:
✗ A note cannot be longer than 200 characters.The note is echoed once, dimmed, under the banner and before the first environment is published, so you can see what is about to be stored.
Multiple environments
publish is the only subcommand that takes more than one environment. Resolution order is --env, then ENVLESS_ENV, then the environments array in .envless, then a multi-select picker on a TTY. Duplicate entries are collapsed, so --env production,production publishes once.
envless publish --env staging,production
ENVLESS_ENV=staging,production envless publish Cut for the release branchEvery environment is resolved up front, before the first snapshot is written. An identifier that matches nothing stops the whole run and nothing is published at all:
$ envless publish --env staging,producton
┌ envless version publish
✗ Environment producton not found in this project.
└ Aborted.Once resolution is through, each environment is published on its own, so a failure late in the list does not undo a snapshot that already landed. The run continues and the summary says what got through, then exits 1:
✔ Staging → v8 with 14 variables
✗ Could not publish Production (Something went wrong while publishing the version.).
│
└ ! 1 published, 1 failed.list, get and restore each take exactly one environment. A comma-separated --env fails with ✗ This command takes a single environment, but 2 environments were named. Pass --env <slug> to pick one. and exits 1, so run them once per environment. A directory whose .envless links several environments fails the same way, because the binding supplies the whole list, and the message names the source instead, ✗ This command takes a single environment, but .envless links 3 environments. Pass --env <slug> to pick one.
Listing
envless version list --env production
envless version ls --env staging
envless version list --env production --jsonPlain output, with no banner and no spinner, so it pipes cleanly. Newest first.
$ envless version list --env production
LABEL VARS NOTE AUTHOR CREATED
v13 14 Before the Stripe migration Benjamin 2026-07-28
v12 14 - Benjamin 2026-07-21
v11 12 Cut for the release branch Ada Lovelace 2026-07-02VARS is how many variables the snapshot holds, counted when it was published, not how many the environment holds today. NOTE shows - when the version was published without one, AUTHOR shows - when no author name was recorded, and CREATED is the date part of the timestamp.
An empty environment is not an error. It exits 0 and says what to do next:
$ envless version list --env production
› No versions in Production yet. Publish one with envless publish.On Free the table holds a single row, the latest version, however many were published; the older ones are stored but not listed until the workspace is on Dev or above.
--json prints the version rows exactly as the API returned them and nothing else, so no header line gets in the way of a parser. An environment with no versions prints [].
$ envless version list --env production --json | jq -r '.[].label'
v13
v12
v11The four fields the table uses are label, variableCount, note and createdAt, plus the stored author name. The rest of each row is passed through untouched:
[
{
"id": 118,
"uniqueId": "0192b7a1-4c62-7d19-8f03-2a6b5c4e9d71",
"environmentId": "0192b6f5-8e77-7bd2-a3c6-1d4e8f0b62a9",
"workspaceId": "0192b6e2-11a4-7c88-9b52-4d3f7a1e6c05",
"label": "v13",
"note": "Before the Stripe migration",
"variableCount": 14,
"createdByUserId": "0192b6e0-7f31-7a44-9c18-5b2d8e4f1a63",
"createdByName": "Benjamin",
"createdByEmail": "benjamin@acme.dev",
"createdAt": "2026-07-28T09:12:04.000Z",
"createdBy": {
"uniqueId": "0192b6e0-7f31-7a44-9c18-5b2d8e4f1a63",
"name": "Benjamin",
"email": "benjamin@acme.dev",
"avatar": null
}
}
]If the list call fails, list prints ✗ Could not load versions (<reason>). and exits 1.
Inspecting a version
envless version get v13 --env production
envless version get 0192b7a1-4c62-7d19-8f03-2a6b5c4e9d71 --env production
envless version get v13 --env production --jsonLabels count up per environment, so v13 in staging and v13 in production are different snapshots. A label is only looked for in the environment you asked about.
$ envless version get v13 --env production
LABEL v13
ENV production
VARS 3
NOTE Before the Stripe migration
AUTHOR Benjamin
CREATED 2026-07-28
NAME VALUE TYPE VISIBILITY
DATABASE_URL •••••••• string server
REDIS_URL •••••••• string server
STRIPE_KEY •••••••• string server
Values are hidden. Add --reveal to decrypt them.Values are masked by default, so a get transcript is safe to paste into a pull request or a CI log. Rows are sorted by name. A snapshot taken from an empty environment prints This version holds no variables. instead of the table.
Add --reveal to decrypt the snapshot and read what a variable actually held at that point, which is the whole reason to inspect a version before restoring it:
$ envless version get v13 --env production --reveal
LABEL v13
ENV production
VARS 3
NOTE Before the Stripe migration
AUTHOR Benjamin
CREATED 2026-07-28
NAME VALUE TYPE VISIBILITY
DATABASE_URL postgres://user@db.acme.dev/app string server
REDIS_URL redis://cache.acme.dev:6379 string server
STRIPE_KEY sk_live_51Hxxxxxxxxxxxx string serverA snapshot entry whose stored value was empty shows as (empty) in the revealed table, so an empty value is never confused with one that failed to arrive.
--reveal is the only thing in this group that needs the workspace key, so it is the only one that requires an unlocked workspace. Run envless passphrase set first, or set ENVLESS_KEY. A key rotation re-encrypts version history alongside the live variables, so an older version still opens with the current key. What fails is an entry a rotation could not decrypt and skipped, which stays on an earlier key. Decryption runs before anything is printed, so the first entry that fails stops the whole command with ✗ Could not decrypt <name> in <label>. This version may predate the current workspace key. and exits 1, rather than printing something wrong.
A label or id that does not exist in that environment exits 1. On Free the same message is what an older version gets, because the label is looked up in the list and the list only carries the latest one:
$ envless version get v99 --env production
✗ Version v99 not found in Production. Run envless version list to see the labels.With no version at all it exits 2 before any network call:
$ envless version get
✗ Pass a version. Try envless version get <label>, or envless version list to see them.--json follows the same rule. It reports the version header and one entry per variable with its name and metadata, and includes value only when you also pass --reveal. The stored ciphertext is never printed either way, so nothing encrypted lands in a log or a CI artifact.
{
"label": "v13",
"environment": "production",
"note": "Before the Stripe migration",
"variableCount": 3,
"createdAt": "2026-07-28T09:12:04.000Z",
"variables": [
{
"name": "DATABASE_URL",
"type": "string",
"visibility": "server",
"isSecret": true,
"required": false
}
]
}Trimmed to one variable here. Add --reveal to include the decrypted value on each entry, which is the only way this group reads a value and the only thing in it that needs the workspace key.
Restoring
envless version restore v11 --env production
envless version rollback v11 --env production --yesRestore makes the environment match the snapshot again. A variable in the snapshot that is missing today is recreated, one whose stored value or metadata differs is set back, and one that exists today but was not in the snapshot is deleted. It is a single server-side transaction per environment, so there is no half-restored state.
$ envless version restore v11 --env production
┌ envless version restore
✔ v11 (12 variables)
│
◆ Restore v11 into Production? It replaces the variables that are there now.
│ ○ Yes / ● No
│
✔ Production → 3 added, 6 changed, 1 removed
│
└ ✓ Production matches v11 again. Run envless sync to refresh your .env files.changed counts the variables whose value, type, visibility, secrecy, required flag or stored default differed from the snapshot. A restore with nothing to do reports 0 added, 0 changed, 0 removed and exits 0.
The confirmation names the label and the environment, and it covers the whole restore, since restore works on one environment at a time. The default answer is No. Declining writes nothing, prints Aborted. and exits 1.
--yes (or -y) skips the confirmation. Without a TTY and without --yes, restore refuses with ✗ Refusing to run unattended. Pass --yes to confirm. and exits 1, so an unattended job can never quietly replace an environment.
Restoring a version is also a step-up guarded action. Unless you verified a code for this workspace in the last hour, the CLI prompts for one after the confirmation, verifies it, and reruns the restore once. --yes does not skip the code. Without a TTY the restore fails with This action needs a verification code. Run it from an interactive terminal, or do it from the dashboard., so a job that has to run it unattended needs the workspace's Verification Codes setting turned off, see Workspace.
After a restore, the on-disk cache entry for that product, project and environment is cleared. That cache belongs to the @goenvless/env runtime, which writes it at boot and reuses it for 15 minutes, so clearing it means the next boot fetches the restored values 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. Your local .env files are left alone, which is why the outro points at envless sync.
A successful restore fires the variable_version.rolled_back workspace webhook, and every variable it touched gets a history entry naming the label it came from.
Flags
Every subcommand takes the four target flags, --workspace, --product, --project and --env. The rest are per subcommand, and this is the whole set each one reads:
| Subcommand | Every flag it accepts |
|---|---|
publish | --note, --env (several allowed), --workspace, --product, --project |
list | --json, --env (one), --workspace, --product, --project |
get | --reveal, --json, --env (one), --workspace, --product, --project |
restore | --yes / -y, --env (one), --workspace, --product, --project |
| Flag | Used by | Description |
|---|---|---|
--note <text> | publish | Note to store on the version. Wins over the positional words whenever it still has content after trimming. At most 200 characters. |
--env <slugs> | all | Environment slug or unique ID. Comma-separated for several on publish, single only on list, get and restore. Falls back to ENVLESS_ENV, then .envless, then a picker on a TTY. |
--reveal | get | Decrypt the snapshot and print the values instead of the mask. Adds value to every entry under --json. The only flag in this group that needs the workspace key, so the only one that needs an unlocked workspace. |
--json | list, get | Prints the API payload and nothing else. |
--yes, -y | restore | Skip the confirmation. Required without a TTY. |
--workspace <ref> | all | Workspace slug or unique ID. Falls back to .envless, then to the default set by envless workspace use. |
--product <ref> | all | 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> | all | Project slug or unique ID. Falls back to ENVLESS_PROJECT, then .envless, then a picker on a TTY. |
A flag a subcommand does not read is ignored rather than rejected, so envless version list --reveal is just a list and envless publish --json still prints the usual transcript.
The four target flags each need a value. Leaving one empty, either as --project= or as a trailing --project with nothing after it, is refused before any network call with ✗ --project needs a value. Name one explicitly rather than letting it fall back to everything this directory is linked to. and exits 2, so a half-typed flag can never widen the target to whatever the directory is linked to.
Requirements
Every subcommand resolves the same target and needs the same credentials, reads included:
| 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. |
Only get --reveal needs the workspace key, so a locked workspace blocks nothing else in this group. Your role still has to carry the matching permission, one to read versions, one to publish, one to roll back. When it does not, the API message is printed as the failure reason and the exit code is 1:
✗ Could not load versions (The user is not authorized to list versions in this workspace.).
✗ Could not publish Production (The user is not authorized to publish versions in this workspace.).Machine keys are read only, and version rejects them even for list and get, with ✗ Machine keys are read only. Run envless login to write from this machine. For a machine that only needs values at boot, use envless sync or the @goenvless/env runtime instead. See Keys.
Failures
Anything that goes wrong while the target is still being resolved is reported first and exits 1. On list and get there is no banner at all, and on publish and restore these messages land before it:
| Situation | Message |
|---|---|
| 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>. |
| 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. |
After that, failures are per subcommand:
| Situation | Message |
|---|---|
| No environment, no TTY | Missing --env. Pass it inline or run envless link first. |
| Environment list failed | Could not load environments (<reason>). |
| Environment slug not in the project | Environment <slug> not found in this project. |
Several environments on list, get or restore | This command takes a single environment, but <n> environments were named. Pass --env <slug> to pick one., with .envless links <n> environments in place of the middle clause when the list came from the file |
| A publish was rejected | Could not publish <env> (<reason>)., the run continues with the next environment |
| Version list failed | Could not load versions (<reason>). |
| Label or id not in that environment | Version <ref> not found in <env>. Run envless version list to see the labels. |
| A version could not be fetched | Could not load <label> (<reason>). |
A value will not decrypt on get --reveal | Could not decrypt <name> in <label>. This version may predate the current workspace key., nothing is printed |
| A restore was rejected | The API reason on its own, then Aborted. |
restore on the Free plan | Rollback needs a paid plan. Upgrade to restore an earlier version., then Aborted. |
restore unattended without --yes | Refusing to run unattended. Pass --yes to confirm. |
Exit codes
| Code | When |
|---|---|
0 | Every environment was published, the table or the version was printed, an environment with no versions was listed, the restore landed, or bare envless version printed its usage. |
1 | The target could not be resolved, an environment identifier matched nothing, list, get or restore was given several environments, the label was not found, an API call failed, a value in a revealed version could not be decrypted, the confirmation was declined or refused, or at least one environment failed to publish. |
2 | An unknown subcommand, a note longer than 200 characters, a target flag left empty, or get or restore with no <version>. |
Going deeper
How versioning behaves under the hood, and what it deliberately does not promise.