Manage

Version

Publish a snapshot of an environment, then list, inspect and restore it, all from the CLI.

Usage

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

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

SubcommandAlso acceptedWhat it does
publishSnapshot one or more environments and email the rest of the team.
listlsPrint the versions in one environment as a table, newest first.
getShow one version and the names of the variables it holds.
restorerollbackReplace the variables in one environment with a version.

Publishing

bash
envless publish
envless publish Before the Stripe migration
envless version publish --note "Before the Stripe migration" --env staging,production

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

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

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

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

bash
✗ 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.

bash
envless publish --env staging,production
ENVLESS_ENV=staging,production envless publish Cut for the release branch

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

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

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

bash
envless version list --env production
envless version ls --env staging
envless version list --env production --json

Plain output, with no banner and no spinner, so it pipes cleanly. Newest first.

bash
$ 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-02

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

bash
$ 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 [].

bash
$ envless version list --env production --json | jq -r '.[].label'
v13
v12
v11

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

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

bash
envless version get v13 --env production
envless version get 0192b7a1-4c62-7d19-8f03-2a6b5c4e9d71 --env production
envless version get v13 --env production --json

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

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

bash
$ 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  server

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

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

bash
$ 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.

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

bash
envless version restore v11 --env production
envless version rollback v11 --env production --yes

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

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

SubcommandEvery 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
FlagUsed byDescription
--note <text>publishNote to store on the version. Wins over the positional words whenever it still has content after trimming. At most 200 characters.
--env <slugs>allEnvironment 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.
--revealgetDecrypt 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.
--jsonlist, getPrints the API payload and nothing else.
--yes, -yrestoreSkip the confirmation. Required without a TTY.
--workspace <ref>allWorkspace slug or unique ID. Falls back to .envless, then to the default set by envless workspace use.
--product <ref>allProduct 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>allProject 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:

RequirementHow to satisfy it
A signed-in user tokenenvless 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:

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

SituationMessage
Not signed inNot signed in. Run envless login first.
Machine key in ENVLESS_TOKENMachine keys are read only. Run envless login to write from this machine.
No workspace anywhereMissing --workspace. Pass it inline, run envless link, or pick a default with envless workspace use <slug|id>.
No project, no TTYMissing --project. Pass it inline or run envless link first.
Project or product not foundNames the identifier that did not resolve.

After that, failures are per subcommand:

SituationMessage
No environment, no TTYMissing --env. Pass it inline or run envless link first.
Environment list failedCould not load environments (<reason>).
Environment slug not in the projectEnvironment <slug> not found in this project.
Several environments on list, get or restoreThis 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 rejectedCould not publish <env> (<reason>)., the run continues with the next environment
Version list failedCould not load versions (<reason>).
Label or id not in that environmentVersion <ref> not found in <env>. Run envless version list to see the labels.
A version could not be fetchedCould not load <label> (<reason>).
A value will not decrypt on get --revealCould not decrypt <name> in <label>. This version may predate the current workspace key., nothing is printed
A restore was rejectedThe API reason on its own, then Aborted.
restore on the Free planRollback needs a paid plan. Upgrade to restore an earlier version., then Aborted.
restore unattended without --yesRefusing to run unattended. Pass --yes to confirm.

Exit codes

CodeWhen
0Every 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.
1The 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.
2An 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.