Manage

Env

Create, list, rename, duplicate, expose, and delete the environments inside a project, straight from the CLI.

Usage

bash
envless env create [name...]
envless env list
envless env rename <env> [new name...]
envless env duplicate <env> [env...]
envless env expose <env> [--off]
envless env delete <env> [env...]

<env> is an environment slug or a uniqueId. Every subcommand works inside one project, resolved from --project, then ENVLESS_PROJECT, then the .envless file. Nothing here reads or decrypts a value, so none of these commands need an unlocked workspace and none of them ask for a passphrase.

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

duplicate and delete take more than one environment in a single run. rename and expose take exactly one.

Running envless env with no subcommand prints the usage block and exits 0.

bash
$ envless env
envless env
  Create, list, rename, duplicate, expose, and delete environments in a project.

USAGE
  envless env create [name...]            Add an environment to the project
  envless env list                        List the environments in the project
  envless env rename <env> <name...>      Rename an environment
  envless env duplicate <env> [env...]    Copy environments and their variables
  envless env expose <env> [--off]        Make an environment public, or private again
  envless env delete <env> [env...]       Delete environments and their variables

  <env> is a slug or an id. Aliases: ls, add, new, rm, del, remove, copy.
  duplicate and delete take several environments, and ask once for the whole set.

OPTIONS
  --slug <slug>      Slug to create with, or the new slug on rename
  --name <name>      Name to create with, instead of positional words
  --off              Turn public exposure off again on envless env expose
  --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
  --json             Print the list as JSON
  --yes, -y          Skip the delete or expose confirmation

EXAMPLES
  envless env create Staging --slug staging
  envless env list --json
  envless env rename staging Pre release --slug pre-release
  envless env duplicate production
  envless env expose preview
  envless env expose preview --off
  envless env delete staging preview --yes

  Exposing an environment makes every variable in it readable with no token, so it
  asks for confirmation. Turning it off does not.

One line in that block overstates things. duplicate and delete both take several environments, but only delete asks once for the whole set. Duplicating is not destructive and never asks.

Aliases

SubcommandAlso accepted
createadd, new
listls
deleterm, del, remove
duplicatecopy
exposeno alias
renameno alias

Anything else prints ✗ Unknown subcommand: <word>, then the usage block, and exits 2.

Create

bash
envless env create Staging
envless env create "Pre release" --slug pre-release
envless env create --name Staging --slug staging --project billing-api

The name is the positional words after create, joined with a single space, so quoting is optional. --name is only read when you pass no words at all. With neither, an interactive terminal prompts for the name.

bash
$ envless env create "Pre release" --slug pre-release

┌  envless env create
│
✔ Created Pre release in Payments API
│
└  ✓ Slug is pre-release. Add variables with envless var set NAME=value --env pre-release

Names are 2 to 50 characters. The length check and the missing-name check run before any network call, so a bad argument costs you nothing and exits 2. On a non-interactive shell with no name at all, create refuses instead of hanging on the prompt:

bash
$ envless env create
✗ Missing a name. Run envless env create <name>.

--slug is optional. Leave it out and the API derives the slug from the name, so Pre release becomes pre-release. What you pass is slugified server side, so --slug "Pre Release" still lands as pre-release.

Slugs are unique inside a project. Reusing one fails with the API message, for example An environment with slug "staging" already exists in this project., and exits 1. Plan limits surface the same way, as the API message on a failed spinner.

A new environment is not added to .envless for you. Run envless link again if you want envless sync to write a .env.<slug> file for it.

List

bash
envless env list
envless env ls --project billing-api
envless env list --json

Plain output, with no prompt box and no spinner, so it pipes cleanly. list asks for no particular order, so the rows come back sorted by name.

bash
$ envless env list
NAME         SLUG         ID
Development  development  0192b6f4-9c81-7a3e-9f2d-6b5c4a1e8d70
Production   production   0192b6f5-8e77-7bd2-a3c6-1d4e8f0b62a9
Staging      staging      0192b6f5-2ad3-7c14-8b0e-7f2a9c3d5e61

An empty project is not an error. It exits 0 and tells you what to do next:

bash
$ envless env list
› No environments in Payments API yet. Add one with envless env create.

--json prints the environment rows exactly as the API returned them and nothing else, so no header line gets in the way of a parser:

bash
$ envless env list --json | jq -r '.[].slug'
development
production
staging

The three fields the CLI itself uses are uniqueId, name, and slug, which are the three columns of the table. The rest of each row is passed through untouched:

json
[
    {
        "id": 41,
        "uniqueId": "0192b6f5-2ad3-7c14-8b0e-7f2a9c3d5e61",
        "projectId": "0192b6f0-1c44-7ab9-9d31-8ac25e7b4f10",
        "name": "Staging",
        "nameUpdatedAt": null,
        "slug": "staging",
        "variableCount": 18,
        "versionCount": 3,
        "exposed": false,
        "deleted": false,
        "deletedAt": null,
        "createdAt": "2026-05-28T08:30:00.000Z",
        "updatedAt": "2026-06-02T11:04:17.000Z",
        "lastPublishedAt": "2026-06-02T11:04:17.000Z"
    }
]

If the list call fails, list prints ✗ Could not load environments (<reason>). and exits 1.

Rename

bash
envless env rename staging Pre release
envless env rename staging --slug pre-release
envless env rename staging Pre release --slug pre-release

The first word after rename is the environment to change. Everything after it is the new name, joined with a single space. --name is not read here, only --slug is. Pass a name, a --slug, or both, and anything less exits 2 with ✗ Nothing to rename. Pass a new name, or --slug to change the slug.

bash
$ envless env rename staging "Pre release" --slug pre-release

┌  envless env rename
│
✔ Environment Staging (staging)
✔ Renamed to Pre release (pre-release)
│
└  ✓ Slug is now pre-release. Your .envless file and any .env.staging still point at the old slug, so run envless link again.

The slug never follows the name. Renaming Staging to Pre release leaves the slug as staging until you pass --slug yourself, which keeps .env.<slug> filenames stable by default.

Each field is compared against what the environment already has, and only what differs is sent. Rename an environment to the name it already has and nothing is written at all:

bash
$ envless env rename staging Staging

┌  envless env rename
│
✔ Environment Staging (staging)
│
└  Nothing changed.

On rename, --slug is slugified server side exactly as it is on create, so --slug "Pre Release" lands as pre-release. What has to fit 2 to 50 characters is the slugified value, not what you typed. A shorter one comes back as Environment slug should be at least 2 characters long!, and a longer one as Environment slug should be less than 50 characters long!. A value with nothing usable in it, --slug "!!" for example, slugifies to an empty string and gets the same too-short message. A slug already used by another environment in the project is a conflict. All of those exit 1. The client-side comparison runs on the raw flag, before slugification, so --slug Staging on staging is still sent, lands back on staging, and closes on ✓ Renamed to Staging. rather than Nothing changed.

Duplicate

bash
envless env duplicate production
envless env copy production
envless env duplicate staging preview

Copies the environment and every variable in it. The values stay encrypted the whole way, since the copy happens server side and the CLI never holds a plaintext value. The new name is the old one with (copy) appended, and the slug is derived from that, with a numeric suffix if it is already taken.

bash
$ envless env copy production

┌  envless env duplicate
│
✔ Environment Production (production)
✔ Created Production (copy) (production-copy)
│
└  ✓ production-copy is a copy of production, variables included and still encrypted.

Pass several environments to copy each of them in one run. Duplicating is not destructive, so duplicate never asks for confirmation, whatever number of environments you hand it. Several environments at once below covers how a run with more than one target behaves.

Expose

bash
envless env expose preview
envless env expose preview --off
envless env expose preview --yes

Turns the public encrypted JSON feed on or off for one environment. With no flag it exposes, and --off makes it private again. This one takes a single environment, so only the first word after expose is read.

Exposing changes who can reach the environment, so it asks first. The default answer is No, and --yes (or -y) skips the prompt.

bash
$ envless env expose preview

┌  envless env expose
│
✔ Environment Preview (preview)
│
◆  Expose Preview? Every variable in it becomes publicly readable, with no token needed.
│  ○ Yes / ● No
│
✔ Preview is now public
│
└  ✓ Anyone can read preview without a token. Undo it with envless env expose preview --off.

Turning exposure off never asks, since making an environment private again cannot cost you anything:

bash
$ envless env expose preview --off

┌  envless env expose
│
✔ Environment Preview (preview)
✔ Preview is now private
│
└  ✓ preview is private again, so reading it needs a token.

The public or private word on the last two lines is read back from the API response, not from the flag you passed, so what you see is the state the environment ended up in.

The feed serves ciphertext, never plaintext. Exposing means the encrypted export can be fetched with no Authorization header, and decrypting it still needs the workspace passphrase. See Virtual environments for the URL shape and the response schema.

Two rules can refuse an exposure. Both come back as the API message on a failed spinner, then Aborted. and exit 1:

bash
✖ Enable Virtual Envs for this workspace first.
✖ A private product's environment cannot be exposed publicly.

Virtual Envs is a workspace-level switch that a workspace owner turns on, and an environment inside a private product can never be exposed. Both checks only run while exposing, so --off is never blocked by either.

Delete

bash
envless env delete staging
envless env rm staging --yes
envless env delete staging preview --yes

Deletes the environment and every variable in it. On a terminal you get a confirmation that defaults to No. --yes (or -y) skips it.

bash
$ envless env delete staging --yes

┌  envless env delete
│
✔ Environment Staging (staging)
✔ Deleted Staging
│
└  ✓ staging is gone, and its cached copy on this machine was cleared.

The on-disk cache entry for that product, project, and environment is cleared as part of the delete, so a stale ciphertext bundle in ~/.envless/cache/ cannot keep serving a deleted environment on this machine. Other machines still hold their own copy until they fetch again.

Declining the confirmation aborts with Aborted. and exits 1. Without a TTY and without --yes, it refuses rather than guessing. The environment is resolved before the question is asked either way, so the refusal lands inside the same box and ends on Aborted. too:

bash
$ envless env delete staging

┌  envless env delete
│
✔ Environment Staging (staging)
✗ Refusing to run unattended. Pass --yes to confirm.
│
└  Aborted.

Deleting an environment 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 delete once. --yes does not skip the code. Without a TTY the delete 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.

Several environments at once

duplicate and delete take a list of environments. Repeated words are collapsed, so envless env delete staging staging deletes one environment, and two identifiers that point at the same environment count once.

bash
envless env duplicate staging preview
envless env delete staging preview --yes

Every identifier is resolved before anything is copied or deleted. One word that matches nothing stops the run at the resolve step, so nothing is touched:

bash
$ envless env delete staging nope

┌  envless env delete
│
✖ Environment nope not found in Payments API.
│
└  Aborted.

More than one bad word is named on the same line, as Environments not found in Payments API: nope, gone.

Delete then asks once for the whole set, naming every environment. There is no prompt per environment, so a single No keeps all of them:

bash
◆  Delete Staging, Preview from Payments API? Every variable in them is removed.
│  ○ Yes / ● No

After that each environment gets its own line. When they all land, the closing line names every one of them:

bash
└  ✓ staging, preview are gone, and their cached copies on this machine were cleared.

One failure does not stop the rest of the run:

bash
$ envless env delete staging preview --yes

┌  envless env delete
│
✔ Resolved Staging (staging), Preview (preview)
✔ Deleted Staging
✖ Preview was kept (Something went wrong while deleting the environment.)
│
└  ! 1 deleted, 1 kept.

A partial run exits 1. So does a run where every target failed, which aborts with Aborted. instead of printing a summary. duplicate reads the same way, with was not copied on the failing line and ! 1 copied, 1 failed. at the end:

bash
$ envless env duplicate staging preview

┌  envless env duplicate
│
✔ Resolved Staging (staging), Preview (preview)
✔ Created Staging (copy) (staging-copy)
✖ Preview was not copied (Plan limit reached.)
│
└  ! 1 copied, 1 failed.

A single-target run is unchanged by any of this. It keeps the one-line messages shown above, and reports the API reason on its own rather than as <name> was kept (<reason>).

envless product delete and envless project delete take lists the same way, with the same up-front resolution and the same single confirmation.

Resolving the target

All six subcommands resolve the workspace, product, and project the same way, right after their own argument checks and before anything else runs. The workspace comes from --workspace first, the .envless file second, and the default set by envless workspace use third. The product comes from --product, then ENVLESS_PRODUCT, then .envless, and it only matters when the same project slug exists in more than one product. The project comes from --project, then ENVLESS_PROJECT, then .envless.

If the project is still unknown and the terminal is interactive, you get a project picker. If it is not interactive, the command stops:

bash
$ envless env list
✗ Missing --project. Pass it inline or run envless link first.

A missing workspace reads the same way, with ✗ Missing --workspace. Pass it inline, run envless link, or pick a default with envless workspace use <slug|id>.. Both exit 1. An identifier that does not resolve names itself, for example ✗ Project billing-api not found in this workspace., and also exits 1.

--env is not used by these commands. The environment is always positional, and it accepts a slug or a uniqueId. An identifier that matches nothing fails with Environment <word> not found in <project>. and exits 1.

Flags

FlagDescription
--name <name>create only. The name to create with, read only when you pass no positional words.
--slug <slug>The slug to create with, or the new slug on rename. Slugified server side either way.
--offexpose only. Makes the environment private again instead of exposing it, and skips the confirmation.
--project <slug|id>Project to work in. Falls back to ENVLESS_PROJECT, then .envless.
--product <slug|id>Product to disambiguate a project slug. Falls back to ENVLESS_PRODUCT, then .envless.
--workspace <slug|id>Workspace to work in. Falls back to .envless, then to the default set by envless workspace use.
--jsonlist only. Prints the raw array and nothing else.
--yes, -ydelete and expose. Skips the confirmation, and is required when there is no TTY. No effect on --off, which never asks.

Requirements

You must be signed in with envless login. With no session at all, every subcommand stops with ✗ Not signed in. Run envless login first. and exits 1.

These are write commands, so they need a device login rather than a machine key. An ev_sk_ token in ENVLESS_TOKEN is rejected up front, including for list:

bash
$ envless env list
✗ Machine keys are read only. Run envless login to write from this machine.

No workspace key is needed. A locked workspace does not block any of this, and there is no reason to run envless passphrase set first, because environments are metadata and no variable is decrypted. Your role still has to carry the matching environment permission, and when it does not, the API message is printed as the failure reason and the exit code is 1.

Exit codes

CodeWhen
0Success, including the usage block with no subcommand, an empty list, a rename that changed nothing, and every target of a multi-environment run landing.
1Not signed in, machine key rejected, workspace or product or project unresolved, environment not found, prompt cancelled, confirmation declined or refused, an exposure the API refused, or any other API error. In a run with several environments, one failed target is enough, so a partial delete or duplicate also exits 1.
2Unknown subcommand, missing environment identifier, missing name on a non-interactive create, a name outside 2 to 50 characters, or a rename with neither a name nor --slug.