Usage
envless project create [name...] [--env <a,b>] [--link] [--slug <slug>] [--product <pr>]
envless project list [--json] [--product <pr>] [--workspace <w>]
envless project update <slug|id> [new name...] [--slug <new-slug>] [--product <pr>]
envless project duplicate <slug|id> [<slug|id> ...] [--product <pr>]
envless project delete <slug|id> [<slug|id> ...] [--yes]A project holds the environments that hold your variables, so envless project sits one level below envless product and one level above envless link. None of the subcommands touch variable values, which means no passphrase and no workspace key are involved. See Concepts for how a workspace, a product, a project, and an environment nest.
The envless command ships in @goenvless/cli. Install it with npm i -g @goenvless/cli@latest.
Running envless project 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 |
|---|---|---|
create | add, new | One new project |
list | ls | Every project you can read |
update | edit | One project |
duplicate | copy | As many projects as you pass |
delete | rm, remove, del | As many projects as you pass |
envless project rename still works and runs the same code as update, so both accept the same arguments and behave identically. Invoked as rename, the intro line and the missing-project message keep saying rename.
update takes a single project, because a new name and a new slug cannot be shared. duplicate and delete take a list, and every identifier is resolved before either of them writes anything. Repeating an identifier, or passing a slug together with the unique ID of the same project, counts once.
Create
envless project create Payments API
envless project create --name 'Payments API' --slug payments-api --product web
envless project create Payments API --env development,production --linkThe name can be given as bare words after create, 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.
The name is checked locally before the intro line and before any request goes out. Under 3 characters fails with A project name needs at least 3 characters. and over 50 with A project name can be at most 50 characters., both exiting 2. The interactive prompt applies the same rule inline, so a bad answer is asked again instead of ending the command. Cancelling the prompt prints Aborted. and exits 1.
A name is required when output is not a TTY. In that case the CLI prints ✗ Missing a project name. Run envless project create <name>. and exits 2 rather than waiting on a prompt that nobody can answer.
A project always lands inside a product. The product comes from --product, then ENVLESS_PRODUCT, then the product field of .envless. With none of those set, create asks which product to use, and a workspace holding exactly one product skips the question. Without --slug the API derives the slug from the name. What you pass to --slug is slugified the same way and has to be 3 to 50 characters once slugified, so --slug 'Payments API' lands as payments-api and --slug ab comes back as Project slug should be at least 3 characters long! The success line always prints the slug the API returned, so it is safe to copy into a .envless file or a later --project flag.
A product is required too when output is not a TTY and the workspace holds more than one. There is nobody to answer the picker, so the CLI prints ✗ Missing --product. Acme has 3 products, so pass one inline or run envless link first. and exits 1. A workspace holding exactly one product still resolves on its own, so only the ambiguous case needs the flag.
$ envless project create Payments API --env development,production --link
┌ envless project create
│
◇ Choose a product
│ Web (web)
│
✔ Project Payments API payments-api
✔ Environment development development
✔ Environment production production
│
└ ✓ Created Payments API, 2 environments, a .envless file. Next, run envless var set NAME=value.A workspace with no products at all does not stop the command. No productId is sent, and the API places the project in a product called Default that it creates on the spot. The run exits 0. Note that --link then writes a .envless with no product key even though the project lives in default, which is fine because default is the only product in that workspace, so the slug resolves without one.
Environments in the same command
--env takes a comma-separated list, exact duplicates are dropped, and each value is treated as an environment name so the API derives its slug. Two values that differ only in case or punctuation are not exact duplicates, so both are sent and the second one is refused on the slug it derives. They are created one at a time, after the project exists.
envless project create Payments API --env development,staging,productionA failing environment prints ✖ Environment staging was not created (<api error>). and the loop moves on to the next one, so one bad name never costs you the rest. The project itself still exists, the outro counts what happened, and the exit code is 1.
$ envless project create Payments API --env development,Development
┌ envless project create
✔ Project Payments API payments-api
✔ Environment development development
✖ Environment Development was not created (An environment with slug "development" already exists in this project.).
│
└ ! Created Payments API, 1 environment. 1 environment could not be created.Linking the directory
--link writes a .envless file for the project it just created, holding the workspace slug, the product slug, the new project slug, and only the environments that were actually created.
{
"workspace": "acme",
"product": "web",
"project": "payments-api",
"environments": ["development", "production"]
}With no environment created, --link refuses rather than write a file that cannot be synced: ⚠ Not linking this directory. A .envless file needs at least one environment, so pass --env development,production or run envless link once an environment exists. The project is still created. The exit code is 0 when no environment was asked for, and 1 when the ones asked for all failed.
The outro closes with the next step. It points at envless var set NAME=value when environments exist, and at envless env create <name> when they do not.
Server-side refusals are printed as they came back and exit 1. The common ones are a slug clash (A project with slug "payments-api" already exists in this product.), a plan limit (Plan limit reached.), and missing permission (The user is not authorized to create projects in this workspace.).
List
envless project list
envless project list --product web
envless project list --jsonPrints one row per project you can read, with the name, the slug, the product, and the unique ID. A private product you hold no grant for is left out, unless your role can manage private products, which owners and admins can, in which case every private product is listed too. There is no intro block and no spinner, so the output pipes cleanly into grep, head, or anything else.
$ envless project list
NAME SLUG PRODUCT ID
Payments API payments-api web 0192a1b2-c3d4-7000-8000-000000000021
Checkout checkout web 0192a1b2-c3d4-7000-8000-000000000022
Marketing marketing growth 0192a1b2-c3d4-7000-8000-000000000023The PRODUCT column names the product the project sits in. Every project belongs to one, so the column is never empty. The listing covers the whole workspace unless a product is in play. A product filter is applied only when --product, ENVLESS_PRODUCT, or the product field of .envless names one, which makes envless project list inside a linked directory narrower than the same command run outside it.
Use --json for scripts. It prints the array and nothing else, including an empty [] when there is nothing to list. Each object carries the four columns of the table plus the rest of the project record, so read the fields you need by name instead of relying on the shape.
[
{
"uniqueId": "0192a1b2-c3d4-7000-8000-000000000021",
"name": "Payments API",
"slug": "payments-api",
"productSlug": "web",
"productName": "Web",
"environmentCount": 2,
"variableCount": 34,
"locked": false,
"createdAt": "2026-05-28T08:30:00.000Z",
"updatedAt": "2026-05-28T08:30:00.000Z"
}
]In the table view an empty result prints › No projects yet. Run envless project create <name>. and still exits 0, so a fresh workspace does not fail a script. With a product in play the scope is named, as in › No projects in Web yet. Run envless project create <name>. A failed lookup prints ✗ Could not load projects (<error>). and exits 1.
Update
envless project update payments Payments API
envless project update payments --slug payments-api
envless project update payments --name 'Payments API' --slug payments-api
envless project rename payments Payments APIThe first word after update is the project, given as its exact slug or unique ID. Everything after it is the new display name, joined with single spaces. --name is read only when no words follow the identifier. --slug sets a new slug. Pass a name, a slug, or both.
Missing the project reports ✗ Missing the project to update. Run envless project update <slug|id> <new name>. and exits 2. Passing neither a new name nor --slug reports ✗ Nothing to update. Pass a new name, --slug <new-slug>, or both. and exits 2. The 3 to 50 character rule applies to the new name and also exits 2. Run through rename, the missing-project message names envless project rename instead.
$ envless project update payments --slug payments-api
┌ envless project update
✔ Project Payments payments
✔ Project Payments payments-api
│
└ ✓ Updated Payments. The slug is now payments-api, so update any .envless that still pins payments.$ envless project rename payments Payments API --slug payments-api
┌ envless project rename
✔ Project Payments payments
✔ Project Payments API payments-api
│
└ ✓ Renamed to Payments API. The slug is now payments-api, so update any .envless that still pins payments.Only the fields that actually differ are sent. If the project already carries that name and that slug, the command prints Already set that way, nothing changed., calls nothing, and exits 0. When the slug changes, the outro names both the old and the new slug, because any .envless file or --project flag still pinning the old one stops resolving.
--slug is slugified server side, so --slug 'Payments API' lands as payments-api. What is left has to be 3 to 50 characters, and a shorter one comes back as Project slug should be at least 3 characters long! and exits 1. The check for whether anything differs runs on your machine, on the value you typed, so --slug 'Payments API' on a project already at payments-api is a real write that lands on the same slug.
A slug that exists in more than one product of the workspace cannot be resolved from the slug alone. The CLI fails the lookup with Could not resolve project (Project "payments" exists in more than one product in this workspace. Use the project unique ID instead of its slug.), prints Aborted., and exits 1. Pass --product web to narrow it, or use the unique ID from envless project list.
Server-side refusals exit 1 and are printed as they came back. A slug clash reads A project with slug "payments-api" already exists in this product. A project put out of reach by a plan downgrade reads This project exceeds your plan limit. Upgrade to make changes. Missing permission reads The user is not authorized to update projects in this workspace.
Duplicate
envless project duplicate payments-api
envless project duplicate payments-api checkout
envless project copy payments-apiA copy lands in the same product as the original and carries every environment of the project with every variable in it. The copy is made server side and the CLI never holds a value, so the ciphertext is moved as it is and there is no reason to run envless passphrase set first.
The new name is the old one with (copy) appended, and the slug is derived from that name, so Payments API becomes Payments API (copy) at payments-api-copy. A numeric suffix is added when that slug is already used in the product, giving payments-api-copy-2, then payments-api-copy-3.
$ envless project duplicate payments-api checkout
┌ envless project duplicate
✔ Resolved payments-api, checkout
✔ Payments API (copy) (payments-api-copy)
✔ Checkout (copy) (checkout-copy)
│
└ ✓ 2 duplicated. Every copy carries the environments and variables of the original, still encrypted.The environments inside a copy keep their own names and slugs, so only the project name and slug change and the .env.<slug> filenames envless sync writes still line up. Each variable is copied with its type, visibility, secret flag, required flag, and both its encrypted value and its encrypted default value. Version history is not copied, so the copy starts with none.
There is no confirmation, because nothing is overwritten or removed. Every identifier is resolved before the first copy is made, so an identifier that matches nothing stops the run with Not in Acme: payments-2. Nothing was copied. and exits 1. A lookup that fails outright stops it just as early, with ✖ Could not resolve project payments-2 (<error>).
Once the set has resolved, copies are made one at a time and a refusal on one project never stops the rest. The outro counts what happened and the exit code is 1 when anything failed.
$ envless project duplicate payments-api checkout
┌ envless project duplicate
✔ Resolved payments-api, checkout
✔ Payments API (copy) (payments-api-copy)
✖ checkout failed (Plan limit reached.)
│
└ ! 1 duplicated, 1 failed.Slugs are resolved the same way as on update, so a slug living in more than one product needs --product web or the unique ID. --product applies to every identifier in the run.
Delete
envless project delete payments-api
envless project delete payments-api checkout --yesProjects are matched by exact slug or unique ID, the two values envless project list prints. A name is not accepted. Every identifier is resolved before the confirmation, so a miss stops the run before anything is deleted, with Project payments-api not found in Acme. for one identifier and Projects not found in Acme: payments-api, checkout for several. Both exit 1.
$ envless project delete payments-api
┌ envless project delete
✔ Project Payments API payments-api
│
◇ Delete Payments API? Every environment and variable in it is removed.
│ Yes
│
✔ Deleted Payments API (with 3 environments, 21 variables)
│
└ ✓ Payments API is gone. Run envless link again if this directory pointed at it.There is one confirmation for the whole set and it names every project in it. 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 project delete payments-api checkout
┌ envless project delete
✔ Resolved Payments API payments-api, Checkout checkout
│
◇ Delete Payments API, Checkout? Every environment and variable in them is removed.
│ Yes
│
✔ Deleted Payments API (with 3 environments, 21 variables)
✔ Deleted Checkout (with 2 environments, 9 variables)
│
└ ✓ Payments API, Checkout are gone. Run envless link again if this directory pointed at one of them.Deletes run one at a time after the confirmation, and a refusal on one project never stops the rest. The outro then reads ! 1 deleted, 1 kept. and the exit code is 1. When every project was kept, the run ends on Aborted. and also exits 1.
This is never refused for holding environments. Every environment and every variable inside the project goes with it, so read the confirmation before answering. To keep a copy first, run envless project duplicate on it.
Without --yes on a non-TTY the command stops before deleting anything with ✗ Refusing to run unattended. Pass --yes to confirm. and exits 1. That is the flag to add in CI.
Deleting a project 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.
Deleting the project a directory is linked to leaves a .envless file pointing at nothing, and the next envless sync fails on the lookup. Run envless link again to repoint it.
Flags
| Flag | Description |
|---|---|
--name <name> | create and update. Name for the new project, or the new name. Bare words after the subcommand take precedence over it. |
--slug <slug> | Slug for the new project on create, or the new slug on update. The API slugifies what you pass on either one, and derives the slug from the name when omitted on create. |
--env <a,b> | create only. Comma-separated environment names to create in the new project. |
--link | create only. Writes a .envless file for the new project and the environments that were created. |
--json | list only. Prints the projects as a JSON array and nothing else. |
--yes, -y | delete only. Skips the confirmation prompt. Required when output is not a TTY. |
--product <pr> | Product slug or unique ID. Chooses the product on create, filters list, and narrows an ambiguous slug on update, duplicate and delete. Falls back to ENVLESS_PRODUCT and then to the product field of .envless. |
--workspace <w> | Workspace slug or unique ID. Falls back to the workspace field of .envless, and then to the default set by envless workspace use. |
--project is ignored by every envless project subcommand. update, duplicate and delete take their targets as positional slugs or unique IDs, and create is making a new one.
Requirements
You must be signed in with envless login. No passphrase, ENVLESS_KEY, or --env is needed on list, update, duplicate, and delete, because these commands read and write project records rather than variables. That holds for duplicate too, since the copy of every variable is made server side and stays encrypted.
The workspace comes from --workspace first, the .envless file second, and the default set by envless workspace use third. With none of them, the command stops on ✗ Missing --workspace. Pass it inline, run envless link, or pick a default with envless workspace use <slug|id>. and exits 1.
All five subcommands run through the write context, so a ev_sk_ machine key is rejected with ✗ Machine keys are read only. Run envless login to write from this machine. even for envless project list. Machine keys are for reading variables, as in envless sync. See Keys.
A stale product entry in .envless, a stale ENVLESS_PRODUCT, or a --product flag pointing at something deleted makes any subcommand fail on that lookup with ✗ Product <name> not found in Acme. Clear the value or re-run envless link.
Exit codes
| Code | When |
|---|---|
0 | Success, including a create in a workspace with no products, an empty project list, an update that changed nothing, a create where --link refused because no environment was asked for, and bare envless project printing its usage |
2 | Unknown subcommand, a name outside 3 to 50 characters, no name on a non-TTY, a missing project on update, duplicate or delete, and update with neither a new name nor --slug |
1 | Not signed in, machine key rejected, unresolved workspace, product or project, a create with no product on a non-TTY workspace holding more than one, a cancelled prompt, a declined or unattended confirmation, any API failure, a create where at least one --env environment failed, and a duplicate or delete run where at least one target failed |
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 |
| No workspace from flag, file or default | ✗ 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 |
--product does not resolve | ✗ Product web not found in Acme. and exit 1 |
create in a workspace with no products | No message. The API creates a Default product and the run exits 0 |
create with no product on a non-TTY, more than one in the workspace | ✗ Missing --product. Acme has 3 products, so pass one inline or run envless link first. and exit 1 |
create with no name on a non-TTY | ✗ Missing a project name. Run envless project create <name>. and exit 2 |
create name outside 3 to 50 chars | A project name needs at least 3 characters. or A project name can be at most 50 characters. and exit 2 |
One --env environment fails | ✖ Environment staging was not created (<error>)., the rest continue, the project stays, exit 1 |
--link with no environment created | ⚠ Not linking this directory. A .envless file needs at least one environment, so pass --env development,production or run envless link once an environment exists. and the project is still created |
update with no project | ✗ Missing the project to update. Run envless project update <slug|id> <new name>. and exit 2 |
update with nothing to change | ✗ Nothing to update. Pass a new name, --slug <new-slug>, or both. and exit 2 |
duplicate with no project | ✗ Missing a project. Run envless project duplicate <slug|id> [<slug|id> ...], or envless project list to see them. and exit 2 |
duplicate target not found | Fails the spinner with Not in Acme: <names>. Nothing was copied., prints Aborted., exits 1 |
duplicate refused on one project | <slug> failed (<error>), the rest still run, the outro reads ! 1 duplicated, 1 failed., exit 1 |
delete with no slug or ID | ✗ Missing the project to delete. Run envless project delete <slug|id>. and exit 2 |
| Project not found | Fails the spinner with Project payments not found in Acme., or Projects not found in Acme: <names> for several, prints Aborted., exits 1 |
| Project slug in two products | Fails the spinner with Could not resolve project (Project "payments" exists in more than one product in this workspace. Use the project unique ID instead of its slug.), prints Aborted., exits 1 |
delete declined | Prints Aborted. and exits 1, nothing is deleted |
delete refused on one project | <name> was kept (<error>) with several targets, or the bare API message with one, the rest still run, the outro reads ! 1 deleted, 1 kept., exit 1 |
delete refused on every project | Prints Aborted. and exits 1 |
delete unattended without --yes | ✗ Refusing to run unattended. Pass --yes to confirm. and exit 1 |
| API rejects the write | Prints the API message as it came back and exits 1 |