Usage
envless workspace list [--json]
envless workspace create [name...] [--name <name>] [--slug <slug>]
envless workspace update [<slug|id>] [new name...] [--name <name>] [--slug <slug>] [--workspace <w>]
envless workspace use <slug|id>
envless workspace delete <slug|id> [<slug|id> ...] [--yes]
envless workspace virtual-envs [--off] [--workspace <w>]A workspace is the top of the hierarchy. Everything else lives under it, so envless workspace sits one level above envless product and two above envless link. No subcommand touches a variable, which means no passphrase and no workspace key are involved. See Concepts for how workspaces, products, projects and environments nest.
The envless command ships in @goenvless/cli. Install it with npm i -g @goenvless/cli@latest.
Running envless workspace with no subcommand prints the usage block and exits 0. An unknown subcommand prints ✗ Unknown subcommand: <name>, then the same usage block, and exits 2.
Subcommands
| Subcommand | Also accepted | Targets |
|---|---|---|
list | ls | Every workspace you belong to |
create | add, new | One new workspace |
update | edit | One workspace |
use | no alias | One workspace |
delete | rm, remove, del | As many workspaces as you pass |
virtual-envs | no alias | The resolved workspace, never a positional |
delete is the only subcommand that takes several targets. create and update are single-target because a name is made of several words, so there is no way to tell one target's words from the next one's. use reads only the first word after it, and virtual-envs reads no positional at all.
On delete, every identifier is resolved from one workspace listing before the first deletion, so a target that matches nothing stops the run before anything changes. Repeating an identifier, or passing a slug together with the unique ID of the same workspace, counts once. There is one confirmation for the whole set, not one per workspace.
List
envless workspace list
envless workspace lsPrints one row per workspace you are a member of, with the name, the slug, and the unique ID. There is no intro block and no spinner, so the output pipes cleanly into grep, head, or anything else.
$ envless workspace list
NAME SLUG ID
Acme acme 0192a1b2-c3d4-7000-8000-000000000001
Acme Holdings acme-holdings 0192a1b2-c3d4-7000-8000-000000000002The SLUG and ID columns are the two values every other subcommand accepts as a target, and the two values --workspace accepts. A name is never accepted.
Rows come in your own order, the same one the dashboard's workspace switcher uses for its ⌘1 to ⌘9 shortcuts. It is per member, so reordering never touches anyone else's list. To change it, open the switcher in the dashboard, press the reorder control at the right end of the "N Workspaces" row, and drag the rows (or focus one and press Alt with the arrow keys); the order is saved as you go, and pressing the control again or closing the menu ends reorder mode. Workspaces you join later land at the end until you move them. There is no CLI subcommand for it.
An empty result prints a hint and still exits 0, so a fresh account does not fail a script:
$ envless workspace list
› No workspaces yet. Add one with envless workspace create.--json prints the array exactly as the API returned it and nothing else, including an empty []. That payload is much wider than the table: every stored column plus the computed counts, the subscription, and your role in each workspace.
$ envless workspace list --json
[
{
"id": 12,
"uniqueId": "0192a1b2-c3d4-7000-8000-000000000001",
"name": "Acme",
"slug": "acme",
"icon": null,
"iconUpdatedAt": null,
"nameUpdatedAt": null,
"deleted": false,
"deletedAt": null,
"plan": "free",
"virtualEnvsEnabled": false,
"requireTwoFactor": false,
"requireTwoFactorUpdatedAt": null,
"customDomain": null,
"createdAt": "2026-06-30T14:55:02.000Z",
"updatedAt": "2026-07-27T18:04:11.000Z",
"memberCount": 3,
"productCount": 2,
"projectCount": 5,
"keyCount": 1,
"subscription": null,
"hasLiveSubscription": false,
"role": { "name": "Owner", "isSystem": true, "systemKey": "owner" },
"permissions": ["workspace:update", "workspace:delete", "members:read"]
}
]The role object and the permissions array are longer than shown here. They carry the role you hold in that workspace and the permissions it resolves to, which is what envless role works with.
--json is the only way to read the current state of a workspace from the CLI. There is no status subcommand, so virtualEnvsEnabled tells you whether Virtual Envs are on, requireTwoFactor tells you whether the workspace requires two factor authentication of its members, plan and hasLiveSubscription tell you what the billing side looks like, and customDomain tells you whether a custom domain is attached.
A failed lookup prints ✗ Could not load workspaces (<error>). and exits 1.
Create
envless workspace create Acme Holdings
envless workspace create --name 'Acme Holdings'
envless workspace create Acme Holdings --slug acmeThe name can be given as bare words after create, in which case they are joined with single spaces, or through --name. Bare words win when you pass both. On a terminal with no name at all, the CLI asks for one.
$ envless workspace create Acme Holdings
┌ envless workspace create
✔ Acme Holdings (acme-holdings)
│
└ ✓ Acme Holdings created. Run envless link or envless init in a project directory next.$ envless workspace create
┌ envless workspace create
│
◇ Name this workspace
│ Acme Holdings
│
✔ Acme Holdings (acme-holdings)
│
└ ✓ Acme Holdings created. Run envless link or envless init in a project directory next.Names are checked locally before the request goes out. Under 3 characters fails with A workspace name needs at least 3 characters. and over 50 with A workspace name can be 50 characters at most., both printing Aborted. after the failure line and exiting 2. The interactive prompt applies the same rule inline, so a bad answer is asked again instead of ending the command.
Without --slug, the slug is derived from the name by the API. The success line always prints the slug the API returned, never a local guess, so it is safe to copy into a .envless file or a later --workspace flag. What you pass to --slug is slugified server side too, so --slug 'Acme Holdings' lands as acme-holdings.
create is the one subcommand that does not need a workspace to already be resolvable. It reads your credential and nothing else, so it works in any directory, with no .envless file and no --workspace. Passing --workspace to create has no effect at all.
A name is required when output is not a TTY. In that case the CLI prints ✗ Missing a name. Run envless workspace create <name>. and exits 2 rather than waiting on a prompt that nobody can answer. Cancelling the prompt prints Aborted. and exits 1.
Creating a workspace also points your account at it, because the API selects the workspace it just created, so the dashboard opens on it. That is not what envless workspace use does. use also writes the slug to your local config when there is one to write to, and create writes nothing locally, so commands run in a directory with no .envless keep resolving to whatever that config already held.
Server-side refusals are printed verbatim and exit 1. Workspace slugs are unique across the whole of Envless rather than per account, so a slug someone else already holds comes back as A workspace with this slug already exists. The other one to expect is the account cap, You have reached the maximum of 100 workspaces. A slug that survives slugification but is still too short or too long is refused as Workspace slug should be at least 3 characters long! or Workspace slug should be less than 50 characters long!
Update
envless workspace update --name 'Acme Holdings'
envless workspace update acme Acme Holdings
envless workspace update acme --slug acme-holdings
envless workspace update acme --name 'Acme Holdings' --slug acme-holdingsAny first word after update is the target, and it has to match a workspace by exact slug or unique ID. Everything after it is the new name. With no leading word at all the target is the resolved workspace, which is --workspace first, the .envless file second, and the default from envless workspace use third, so --name is how you rename the one you are working in without naming it.
$ envless workspace update --name 'Acme Holdings'
┌ envless workspace update
✔ Acme (acme)
✔ Acme Holdings (acme)
│
└ ✓ Renamed to Acme Holdings.Any leading word is treated as the target and has to resolve, so a typo stops the run rather than renaming the wrong workspace. envless workspace update acmee Holdings reports ✖ Workspace acmee not found. Pass --name to rename the current workspace instead. and exits 1. To rename the workspace you are working in without naming it, use --name.
--name is read only when no words follow the identifier. --slug sets a new slug and leaves the name alone. Pass one of them, or both in the same run. Renaming never touches the slug, so a workspace keeps the slug it was created with until you ask for a new one.
$ envless workspace update acme --slug acme-holdings
┌ envless workspace update
✔ Acme (acme)
✔ Acme (acme-holdings)
│
└ ✓ Updated Acme. The slug is now acme-holdings, so update any .envless that still pins acme.When the slug changes, the outro names both the old and the new one, because any .envless file or --workspace flag still pinning the old slug stops resolving. Re-run envless link in the directories that pinned it, or edit the workspace field by hand.
Passing nothing to change reports ✗ Nothing to update. Pass a new name, or --slug <new-slug>. and exits 2. With no words and no flags that happens before any network call, with nothing printed after it. With a target that resolved and nothing else, as in envless workspace update acme, the same message comes after the lookup and is followed by Aborted. The 3 to 50 character rule applies to the new name, checked locally before the write goes out, and a failure there prints Aborted. as well.
Only the fields that actually differ are sent. If the workspace already carries that name and that slug, the command prints Already set that way, nothing changed., calls nothing, and exits 0.
$ envless workspace update acme Acme
┌ envless workspace update
✔ Acme (acme)
│
└ Already set that way, nothing changed.The check for whether anything differs runs on your machine, on the value you typed, and the API slugifies --slug after that. So --slug 'Acme Holdings' on a workspace already at acme-holdings is a real write that lands on the same slug, and it does not take the Already set that way, nothing changed. path.
Server-side refusals exit 1 and are printed as they came back. A slug clash reads A workspace with slug "acme" already exists. Missing permission reads The user is not authorized to update the workspace. Renaming and re-slugging need the workspace update permission, which the owner and admin roles hold and a custom role can be given, and the CLI prints whatever the API decided rather than checking your role first. See Roles.
Use
envless workspace use acme-holdings
envless workspace use 0192a1b2-c3d4-7000-8000-000000000002Sets your default workspace. It is the same setting the dashboard uses, so the switch follows you to the browser, and it is written to your local config as well when you have one, which is what any command that does not name a workspace falls back to. The order is --workspace first, the .envless file second, this default third.
$ envless workspace use acme-holdings
┌ envless workspace use
✔ Acme Holdings (acme-holdings)
✔ Switched to Acme Holdings
│
└ ✓ acme-holdings is the default now, for the dashboard and for commands that do not name a workspace. A .envless file still wins inside its own directory.The target is matched by exact slug or unique ID, and it is always the positional word. use never reads --workspace, so passing it changes nothing. A target that matches nothing prints Workspace <name> not found., then Aborted., and exits 1.
With no identifier at all the command stops before any network call with ✗ Missing a workspace. Run envless workspace use <slug|id>, or envless workspace list to see them. and exits 2.
The local half of the switch only happens when a local config file already exists, and envless login is what writes one. Authenticating with ENVLESS_TOKEN alone leaves no config file, so the account default moves and the outro still prints, but nothing is stored on the machine and commands that do not name a workspace resolve exactly as they did before.
A .envless file always wins in its own directory, so use changes nothing for a linked project. It decides what happens in directories that are not linked, and what the dashboard opens on. Switching to a workspace you are not a member of is refused by the API with The user is not part of this workspace to switch to it. and exits 1.
Delete
envless workspace delete acme-legacy
envless workspace delete acme-legacy acme-old --yes
envless workspace rm acme-legacyDeletes whole workspaces, and with them every product, member, role, key and webhook. The API refuses while a single project is left, so a workspace you can actually delete has no project, environment or variable left in it. Workspaces are matched by exact slug or unique ID, the two values envless workspace list prints, and a name is not accepted.
Every identifier is resolved before the confirmation, so a miss stops the run before anything is deleted, with Workspace acme-legacy not found. for one identifier and Workspaces not found: acme-legacy, acme-old for several. Both exit 1.
$ envless workspace delete acme-legacy
┌ envless workspace delete
✔ Acme Legacy (acme-legacy)
│
◇ Delete Acme Legacy? Every product, project, environment and variable in it goes with it.
│ Yes
│
✔ Removed Acme Legacy
│
└ ✓ Acme Legacy deleted.There is one confirmation for the whole set and it names every workspace in it, reading Delete Acme Legacy, Acme Old? Every product, project, environment and variable in them goes with them. for several. The default answer is No. Declining it, or interrupting it, prints Aborted. and exits 1, so a refusal never reads as success. Pass --yes or -y to skip the prompt.
$ envless workspace delete acme-legacy acme-old --yes
┌ envless workspace delete
✔ Acme Legacy (acme-legacy), Acme Old (acme-old)
✔ Removed Acme Legacy
✖ Acme Old was kept (Delete every project in this workspace before deleting it.)
│
└ ! 1 deleted, 1 kept.Deletes run one at a time after the confirmation, and a refusal on one workspace never stops the rest. The exit code is 1 when anything was kept. When every workspace was kept, the run ends on Aborted. instead of a count, and also exits 1.
$ envless workspace delete acme --yes
┌ envless workspace delete
✔ Acme (acme)
✖ You cannot delete your last workspace.
│
└ Aborted.When a deleted workspace is the one your local .envless pins, the outro says so, because nothing in that directory resolves any more:
└ ✓ Acme Legacy deleted. Your .envless still pins acme-legacy, so run envless link again here.Four preconditions live on the API, and the CLI prints whatever came back rather than pre-judging any of them. You need the workspace delete permission, which only the owner role holds and no custom role can be given (You must be the owner to delete the workspace.), it cannot be the last workspace you own (You cannot delete your last workspace.), it has to be at least 7 days old (You cannot delete a workspace within 7 days of creating it.), and it has to be empty of projects (Delete every project in this workspace before deleting it.). Every one of them exits 1.
A live subscription does not stop a delete. It is canceled as part of it, immediately and with no refund for the rest of the period. Everyone who holds billing:manage or workspace:update, which includes you, is emailed a confirmation naming the workspace and the plan that was canceled.
Emptying a workspace first means envless product delete, which takes each product's projects, environments and variables with it, or envless project delete when you want to keep the products. Only the project count is checked, so a workspace holding products with no projects in them can be deleted.
Any successful delete moves your account default to the first workspace you have left, even when the one you deleted was not the one you were defaulting to. That is the API doing it, not the CLI, and envless workspace use overrides it whenever you want a different one.
Without --yes on a non-TTY the command stops before deleting anything with ✗ Refusing to run unattended. Pass --yes to confirm., then Aborted., and exits 1. That is the flag to add in CI.
Deleting a workspace 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.
Virtual Envs
envless workspace virtual-envs
envless workspace virtual-envs --off
envless workspace virtual-envs --workspace acmeFlips the workspace-level switch that lets environments be served as a public encrypted JSON feed. It is off on a new workspace. The bare command turns it on, and --off turns it off again and unexposes every environment in the workspace as it goes.
$ envless workspace virtual-envs
┌ envless workspace virtual-envs
✔ Virtual Envs are on for Acme
│
└ ✓ Acme can expose environments now. Pick one with envless env expose <env>, or turn the feature off again with envless workspace virtual-envs --off.$ envless workspace virtual-envs --off
┌ envless workspace virtual-envs
✔ Virtual Envs are off for Acme
│
└ ✓ Acme cannot expose environments now, so every environment in it needs a token to read.This subcommand reads no positional argument, so the workspace it changes is always the resolved one, from --workspace first, the .envless file second, and the default from envless workspace use third. There is no confirmation either way.
Turning the switch on exposes nothing on its own. It only makes the per-environment toggle available, and that is opt-in one environment at a time with envless env expose.
The gate itself flips back, but what turning it off clears does not. In the same transaction the API sets exposed to false on every exposed environment in the workspace, so the per-environment opt-ins are cleared rather than paused, and each one fires an environment.unexposed webhook. Running the bare command again restores none of them, and every environment you want back has to be exposed again with envless env expose. There is no confirmation before that happens, so note which environments were exposed before you run --off.
There is no virtual-envs status. To read the current state, use envless workspace list --json and look at virtualEnvsEnabled on the workspace you care about.
Refusals are printed as they came back and exit 1. Missing permission reads The user is not authorized to update this workspace., which is the same workspace update permission that renaming needs, so the CLI does not check your role before asking. A workspace the API cannot find reads Workspace not found.
Two factor requirement
A workspace can require that every member has two factor authentication on their own account. There is no subcommand for it. The switch lives in the dashboard, on the workspace Settings page, and writing it needs the workspace:update permission plus a step-up verification code. The CLI's part is reading it, and living with it.
Read it with envless workspace list --json and look at requireTwoFactor on the workspace you care about. requireTwoFactorUpdatedAt carries the moment it last changed, or null if it never has.
Living with it is the part to plan for. A CLI session token is bound to the one workspace it was approved for, and the requirement is checked against the token owner's account before any command runs. So if the workspace your token is bound to requires two factor and your account does not have it on, every envless command using that token is refused, envless workspace list included, whichever workspace you were asking about:
$ envless workspace list
✗ Could not load workspaces (auth.workspace_two_factor_required: This workspace requires two factor authentication. Turn it on for your account to continue.)The fix is on your account, not in the terminal: turn two factor on at Account settings and the same token works again on the next command. Nothing is revoked in the meantime, and the token keeps whatever is left of its 90 days. Signing in again does not help on its own, because a new token approved for the same workspace is refused for the same reason.
An ev_sk_ machine key is refused the same way, with 403 and auth.workspace_two_factor_required, on every public API route including the ones envless sync uses. The check is on the key owner's account, so a pipeline stops the moment the requirement lands on a workspace whose key owner has not enrolled. See Keys and Authentication.
Verification codes
A workspace asks for a step-up verification code before its sensitive actions, envless workspace delete, envless workspace virtual-envs, envless domain claim and envless domain remove among them. The CLI prompts for the code when the server asks, verifies it, and reruns the refused request once; one code covers the rest of the run and the next hour. There is no subcommand to change the setting. The switch lives in the dashboard, on the workspace Settings page under Verification Codes, and turning it off needs the workspace:update permission plus a new code every time, regardless of any elevation you already hold. Every member is emailed when it changes.
Read it with envless workspace list --json and look at requireStepUp on the workspace you care about; requireStepUpUpdatedAt carries the moment it last changed, or null if it never has. With it false, no envless command in that workspace ever prompts for a code, and neither does the dashboard, which is the setting to reach for if a pipeline has to envless push with a CLI token and cannot answer a prompt. Weigh that against what it removes: with the guard off, a leaked CLI token can delete environments and rotate keys on its own. Account-level actions are not covered by the switch and keep asking.
Flags
| Flag | Used by | Description |
|---|---|---|
--name <name> | create, update | Name for a new workspace, or the new name on update. Bare words after the subcommand take precedence over it. |
--slug <slug> | create, update | Slug for a new workspace, or the new slug on update. The API derives one from the name when omitted on create, and slugifies what you pass either way. |
--json | list | Prints the workspaces as a JSON array and nothing else. |
--off | virtual-envs | Turns Virtual Envs off, and unexposes every exposed environment in the workspace. Without it the command turns them on. |
--yes, -y | delete | Skips the confirmation prompt. Required when output is not a TTY. |
--workspace <w> | update, virtual-envs | Workspace slug or unique ID. Falls back to the workspace field of .envless, then to the default set by envless workspace use. list, create, use and delete never read it, because their target is either the whole account or the positional. |
Requirements
Every subcommand needs a device login. No passphrase, ENVLESS_KEY, --project or --env is involved, because these commands read and write workspace records rather than variables. --product is the one exception: update and virtual-envs resolve a product when one is named, then never use it.
| Requirement | How to satisfy it |
|---|---|
| A signed-in user token | envless login, or a user token in ENVLESS_TOKEN. An ev_sk_ machine key is refused. |
| A resolvable workspace | --workspace, a .envless file, or the default set by envless workspace use. Only update and virtual-envs need one. |
All six subcommands refuse an ev_sk_ machine key with ✗ Machine keys are read only. Run envless login to write from this machine. and exit 1, envless workspace list included. Machine keys are for reading variables, as in envless sync. See Keys.
A user token is refused outright when the workspace it was approved for requires two factor authentication and your account does not have it on. That check runs in the API's auth middleware, before the subcommand does anything, so it applies to all six including list. See Two factor requirement.
There is no ENVLESS_WORKSPACE variable. --workspace falls back to the workspace field of .envless, then to the default envless workspace use writes to your local config, and nothing else. That file is only read when it is complete, so a hand-written .envless holding a workspace but no project and no environments is ignored. Run envless link to write a valid one.
list, create, use and delete need only a signed-in session, so they work in any directory with no .envless and no --workspace. Their target is either the whole account or already in the positional. Only update and virtual-envs resolve a workspace first, and on update that happens before the positional target is read, so naming a target does not exempt it. Without one they stop on ✗ Missing --workspace. Pass it inline, run envless link, or pick a default with envless workspace use <slug|id>. and exit 1, and a .envless file or a default pinning a workspace that no longer resolves stops them on ✗ Workspace <pinned> not found. even when the target you named is valid.
On update and virtual-envs the resolver also resolves a product when one is named, and neither command uses it. So a stale product entry in .envless, a stale ENVLESS_PRODUCT, or a --product flag pointing at something deleted makes those two fail on that lookup with ✗ Product <name> not found in Acme. Clear the value or re-run envless link. list, create, use and delete never touch a product.
Exit codes
| Code | When |
|---|---|
0 | Success, an empty workspace list, an update that changed nothing, and bare envless workspace printing its usage |
2 | Unknown subcommand, a name outside 3 to 50 characters, create with no name on a non-TTY, update with nothing to change, use with no identifier, and delete with no identifier |
1 | Not signed in, machine key refused, unresolved workspace or product, a target that does not resolve, cancelled prompt, declined or unattended confirmation, a failed workspace listing, any API refusal, and a delete run where at least one target was kept |
Errors
| Situation | Behavior |
|---|---|
| Not signed in | ✗ Not signed in. Run envless login first. and exit 1 |
ev_sk_ machine key in use | ✗ Machine keys are read only. Run envless login to write from this machine. and exit 1 |
| Workspace requires two factor | Every subcommand fails on its own wrapper around auth.workspace_two_factor_required: This workspace requires two factor authentication. Turn it on for your account to continue. and exits 1 |
| No workspace from flag, file or default | update and virtual-envs only: ✗ Missing --workspace. Pass it inline, run envless link, or pick a default with envless workspace use <slug|id>. and exit 1 |
| Workspace does not resolve | ✗ Workspace acme not found. or ✗ Could not resolve workspace (<error>). and exit 1 |
Stale product in .envless or the env | ✗ Product <name> not found in Acme. and exit 1 |
| Workspace listing failed | ✗ Could not load workspaces (<error>). and exit 1, or the same text on a failed spinner then Aborted. |
create with no name on a non-TTY | ✗ Missing a name. Run envless workspace create <name>. and exit 2 |
create prompt cancelled | Aborted. and exit 1 |
| Name outside 3 to 50 characters | ✗ A workspace name needs at least 3 characters. or ✗ A workspace name can be 50 characters at most., then Aborted., and exit 2 |
update with nothing to change | ✗ Nothing to update. Pass a new name, or --slug <new-slug>. and exit 2, followed by Aborted. when the message comes after the lookup |
update that matches the current values | Already set that way, nothing changed. and exit 0, with no request sent |
use with no identifier | ✗ Missing a workspace. Run envless workspace use <slug|id>, or envless workspace list to see them. and exit 2 |
use target not found | Fails the spinner with Workspace <name> not found., prints Aborted., exits 1 |
delete with no identifier | ✗ Missing a workspace. Run envless workspace delete <slug|id>, or envless workspace list to see them. and exit 2 |
delete target not found | Fails the spinner with Workspace <name> not found., or Workspaces not found: <names> for several, prints Aborted., exits 1, and nothing is deleted |
delete declined | Prints Aborted. and exits 1, nothing is deleted |
delete refused on one workspace | <name> was kept (<error>), the rest still run, the outro reads ! 1 deleted, 1 kept., exit 1 |
delete refused on every workspace | Prints the API message, then Aborted., and exits 1 |
delete unattended without --yes | ✗ Refusing to run unattended. Pass --yes to confirm., then Aborted., and exit 1 |
| API rejects the write | Prints the API message as it came back, then Aborted., and exits 1 |