Usage
envless var set NAME [NAME ...]
envless var set NAME --stdin
envless var set NAME=value [NAME=value ...]
envless var get NAME
envless var list
envless var history NAME
envless var rm NAME [NAME ...]
envless var import <file>var works on named variables, where envless push works on a whole file. It is the command to reach for when you want to rotate one key, read one value back in a script, check who last touched one, or drop a variable that is no longer used. The envless binary ships in @goenvless/cli, a binary only package with nothing importable, so everything on this page is something you run and never something you import.
Nothing leaves your machine in plaintext. Values are encrypted with the workspace key before the request is sent, exactly as the dashboard does it, so the API only ever stores ciphertext. Reading a value back needs the same key, which is why writing or reading a value through set, get, list and rm all require an unlocked workspace. history never touches a value, and neither does a metadata-only set, so both work while the workspace is locked.
Running envless var with no subcommand prints the usage and exits 0. An unrecognised subcommand prints ✗ Unknown subcommand: <name>, then the usage, and exits 2.
Subcommands
| Subcommand | Also accepted | What it does |
|---|---|---|
set | Create or update one or more variables, in one or more environments. | |
get | Print one decrypted value to stdout. | |
list | ls | Print the variables in one environment as a table. |
history | log | Print the change events for one variable in one environment. |
rm | remove, del, delete | Delete variables from one or more environments. |
import | Hand the run to envless push with the file you pass. |
Setting values
Pass the name on its own and set prompts for the value with the input masked. Nothing secret reaches your shell history or the process list, which makes this the form to use for anything you would not paste into a ticket.
envless var set STRIPE_KEY
envless var set STRIPE_KEY DATABASE_URL --env staging,production$ envless var set STRIPE_KEY --env production
┌ envless var set
│
◆ Value for STRIPE_KEY
│ ▪▪▪▪▪▪▪▪▪▪▪▪
│
✔ production → 1 created, 0 updated
created STRIPE_KEY
│
└ ✓ 1 created, 0 updated in 1 environment.For a value that already lives in a file or in another tool, pipe it in with --stdin. It takes exactly one name without a value, reads the pipe to the end, and strips a single trailing newline, so a PEM file or a JSON service account is stored byte for byte.
cat key.pem | envless var set TLS_KEY --stdin
op read op://vault/stripe/key | envless var set STRIPE_KEY --stdin --env productionBoth forms hold the value to the same 20000 character ceiling as the inline form, and neither accepts an empty value, where the inline NAME= form still stores one. Without a TTY and without --stdin, a name with no value is a plain failure rather than a hang, ✗ No value for STRIPE_KEY. Pass STRIPE_KEY=value, or pipe the value in with --stdin. Values are resolved after the target is, so anything that goes wrong at that step exits 1, not 2, including a piped value over the ceiling.
envless var set STRIPE_KEY=sk_live_123 still works, and it is still the form to reach for when the value is not sensitive or when a script already holds it in a variable. Know what it costs. Everything on the command line is written to your shell history file and is visible in the process list, so any other user on the machine can read the value with ps while the command runs. set prints a warning of its own when it sees an inline value. Prefer the prompt or --stdin, and rotate any secret you have already typed inline.
Pass as many pairs as you like, and mix prompted names with inline ones. The name is split on the first =, so a value may contain further = characters, and quoting is the shell's job as usual.
envless var set PORT=3000 --type number --no-secret
envless var set "GREETING=hello world" DATABASE_URL=postgres://localhost:5432/myapp
envless var set STRIPE_KEY PORT=3000Each environment is fetched first, so a name that already exists is updated and a name that does not is created. That lookup ignores case, so envless var set api_key updates a live API_KEY instead of creating a second variable beside it, and the stored name keeps the casing it already had.
The environment line reports both counts and every write is named underneath it.
$ envless var set STRIPE_KEY=sk_live_123 DATABASE_URL=postgres://db --env staging,production
┌ envless var set
│
⚠ STRIPE_KEY, DATABASE_URL were passed as NAME=value, so the value is in this machine process list and in your shell history. Pass the name on its own to be prompted for the value instead.
│
✔ staging → 1 created, 1 updated
created STRIPE_KEY
updated DATABASE_URL
✔ production → 2 created, 0 updated
created STRIPE_KEY
created DATABASE_URL
│
└ ✓ 3 created, 1 updated in 2 environments.Only names are printed, never values, so a var set transcript is safe to paste into a CI log.
Every input is checked before the first network call, so a missing name, a bad pair or a bad flag value costs nothing and exits 2:
✗ Pass at least one NAME=value pair.
✗ =3000 is not a NAME=value pair.
✗ MY-VAR is not a valid name. Use letters, numbers and underscores, and do not start with a number.
✗ PORT was passed more than once.
✗ float is not a valid type. Use string, number, boolean.
✗ internal is not a valid visibility. Use server, client.
✗ Pass --secret or --no-secret, not both.A token with no = at all is read as a name to be prompted for, not as a malformed pair, so envless var set PORT3000 asks for a value rather than failing. Only a token that starts with = is rejected as a pair.
A name may be at most 100 characters. The 20000 character ceiling is measured on the plaintext you pass, before encryption, so the ciphertext that reaches the API is larger than that.
Names are case reserved, not case normalised. A variable is stored with exactly the casing you type, port stays port and is never turned into PORT, but a name has to be unique inside an environment without regard to case. So api_key and API_KEY cannot both exist in one environment, and asking for either one finds the same variable.
Metadata on create
A newly created variable takes the same fields the dashboard sets, with these defaults:
| Field | Default | Change it with |
|---|---|---|
| Type | string | --type with string, number or boolean |
| Visibility | server | --visibility with server or client |
| Secret | yes | --no-secret to store it in the clear, --secret is already the default |
| Required | no | --required |
| Default value | none | --default <value>, encrypted just like the value |
Visibility decides which variables the @goenvless/env runtime is allowed to expose to browser code, through its /client entrypoint. The admin clients, @goenvless/sdk, the Python and Ruby envless-sdk and the PHP envless/sdk, have no part in it. See Server vs client before marking anything client.
--type and --visibility are validated against that exact list, in lower case. --default takes its value as the next argument, so use the --default=<value> form when the value starts with a dash. A value flag left empty, --type= or a trailing --type with nothing after it, is dropped rather than refused, so the field falls back to its default on create and stays untouched on update. That holds for every var subcommand except import, which hands the run to envless push and refuses an empty value flag with ✗ --env needs a value. and exit 2.
Metadata on update
On an existing variable, only the fields you actually passed are sent. envless var set PORT=3000 changes the value and leaves type, visibility, secrecy, required and any stored default exactly as they were, so scripting var set against variables the dashboard configured will not quietly reset them.
envless var set PORT=8080 # value only
envless var set PORT=8080 --type number # value and type
envless var set API_URL=https://acme.dev --visibility client --no-required--secret and --no-secret flip secrecy on update, --required and --no-required flip required, and --default replaces the stored default. Passing neither side of a pair leaves that field untouched.
Changing metadata without the passphrase
Every field except the value is stored in the clear, so editing one needs no workspace key. Pass the name on its own together with the metadata flags and set updates only those fields: it does not prompt for a value, does not ask you to unlock the workspace, and does not send a value at all, so the stored ciphertext is never rewritten.
envless var set PUBLIC_API_URL --visibility client
envless var set PORT --type number --required
envless var set LEGACY_FLAG --no-required --no-secretThis is the form to use when flipping a batch of variables to client so the browser can read them, which is otherwise a lot of dashboard clicks:
for E in local dev prod; do
for V in VITE_API_URL VITE_FIREBASE_API_KEY; do
envless var set "$V" --visibility client --env "$E"
done
doneA bare name with no metadata flag still means "prompt me for a new value", so nothing changes silently. It is the combination of a bare name and at least one metadata flag that makes it a metadata-only edit.
Because there is no value to encrypt, this form can only update. If the variable does not exist yet, set reports NAME does not exist in <environment>, and a new variable needs a value rather than creating an empty one.
Multiple environments
set and rm accept a comma-separated --env, so one command can cover several environments. 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.
envless var set STRIPE_KEY=sk_live_123 --env staging,production
ENVLESS_ENV=staging,production envless var rm OLD_TOKENEach environment is written independently, so one failing environment does not undo a good one. The run continues and the summary says what got through:
⚠ production → 1 created, 0 updated, 1 failed
✗ Could not create DATABASE_URL in production (Variable limit reached.).
│
└ ! 2 created, 0 updated, 1 failed.After a write lands, set and rm clear the on-disk cache entry for that product, project and environment. That cache belongs to the @goenvless/env runtime, which writes it at boot and reuses it for 15 minutes before fetching again, so clearing it means the next boot reads the new value 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. See Caching & Reloads.
Reading one value
envless var get NAME prints the decrypted value and nothing else. It works on a single environment, and every diagnostic goes to stderr, so command substitution stays clean.
$ envless var get STRIPE_KEY --env production
sk_live_123
$ VALUE=$(envless var get STRIPE_KEY --env production)
$ echo "$VALUE"
sk_live_123--json prints the value together with the metadata and the environment slug it came from:
$ envless var get STRIPE_KEY --env production --json
{
"name": "STRIPE_KEY",
"value": "sk_live_123",
"type": "string",
"visibility": "server",
"isSecret": true,
"required": false,
"environment": "production"
}A name that is not set exits 1 and tells you where to look. A variable stored with an empty value prints an empty line and exits 0.
$ envless var get STRIPE_KEY --env production
✗ STRIPE_KEY is not set in production. Run envless var list to see what is.get, list and history take one environment, and that bites more often than it sounds. It is not only --env staging,production that fails: in a directory whose .envless links two or more environments, plain envless var get NAME fails too, because the binding supplies the whole list. The message names the source, ✗ This command takes a single environment, but .envless links 3 environments. Pass --env <slug> to pick one., and exits 1.
Listing
envless var list prints one environment as a table, sorted by name, with the values masked. Nothing is decrypted on screen unless you ask for it.
$ envless var list --env production
NAME VALUE TYPE VISIBILITY
DATABASE_URL •••••••• string server
NEXT_PUBLIC_URL •••••••• string client
PORT •••••••• number server--reveal swaps the mask for the plaintext. Values are still decrypted locally, so --reveal needs the key just like the masked view does.
$ envless var list --env production --reveal
NAME VALUE TYPE VISIBILITY
DATABASE_URL postgres://localhost:5432/myapp string server
NEXT_PUBLIC_URL https://acme.dev string client
PORT 3000 number serverA variable stored with an empty value shows as (empty) in both the masked and the revealed table, so an empty value is never confused with a mask. A value that could not be decrypted reads the same way, so (empty) is not proof that the value is empty. As long as one other value decrypted, list warns above the table and still exits 0:
⚠ 1 of 3 variables could not be decrypted and were left out: DATABASE_URL. Check your passphrase or ENVLESS_KEY.When nothing at all decrypts, there is no table. The run stops on ✗ Could not decrypt <name>. Run envless passphrase set to unlock this workspace. and exits 1.
--json prints an array instead of a table. The value field is only present with --reveal, which keeps a plain --json listing safe to log:
$ envless var list --env production --json
[
{
"name": "DATABASE_URL",
"type": "string",
"visibility": "server",
"isSecret": true,
"required": false
}
]An empty environment prints [] under --json, and a hint otherwise. Both exit 0.
$ envless var list --env production
› No variables in production yet. Add one with envless var set NAME=value.History
envless var history STRIPE_KEY --env production
envless var log STRIPE_KEY --env production
envless var history STRIPE_KEY --env production --jsonhistory prints what has happened to one variable in one environment, newest first. Plain output, with no prompt box and no spinner, so it pipes cleanly.
$ envless var history STRIPE_KEY --env production
WHEN WHAT WHO
2026-07-27 18:04:11 updated Benjamin Azwedo
2026-07-21 09:12:47 updated ci@acme.dev
2026-06-30 14:55:02 created Benjamin Azwedo| Column | What it holds |
|---|---|
WHEN | The recorded timestamp trimmed to the second. It is the stored UTC value, not your local time, and shows as - on an event with no timestamp. |
WHAT | The event type: created, updated, deleted or restored. |
WHO | The name on the event, falling back to the email address, and - when the event carries neither. |
The events are sorted by the CLI as well as by the API, so the newest line is always first whatever order the response arrived in.
On the Free plan the table holds a single line, the latest event; the older ones are stored but need Dev or above to be listed. See Plans and billing.
No column ever holds a value, which is what makes history the one var subcommand that works on a locked workspace. It needs a signed-in user and read access to variables, not the workspace key, so there is no reason to run envless passphrase set first.
The name is matched without regard to case, and against the variables the environment holds right now. That means a variable you have already removed can no longer be looked up, and a name that was never there exits 1 before any history call:
$ envless var history NOPE --env production
✗ NOPE is not set in production.A variable that exists but has no events yet is not an error. It exits 0:
$ envless var history PORT --env production
› No history for PORT in production yet.--json prints the event objects as the API returned them, sorted but otherwise untouched, so a parser gets the metadata and the actor fields the table leaves out. An empty history prints [], and both exit 0:
$ envless var history STRIPE_KEY --env production --json
[
{
"id": 412,
"uniqueId": "0192b7c1-45ad-7f02-9c31-2e6b8d40a7f5",
"variableId": "0192b7bf-91e2-7a44-8f13-5c0d7a91b6e2",
"environmentId": "0192b6f5-8e77-7bd2-a3c6-1d4e8f0b62a9",
"workspaceId": "0192b6f0-2c11-7a90-b8d4-9f2a1c3e5d77",
"userId": "0192b6ef-77aa-7c31-9b62-4e8d0a15c9f3",
"userName": "Benjamin Azwedo",
"userEmail": "benjamin@acme.dev",
"type": "updated",
"metadata": null,
"createdAt": "2026-07-27T18:04:11.000Z",
"user": {
"uniqueId": "0192b6ef-77aa-7c31-9b62-4e8d0a15c9f3",
"name": "Benjamin Azwedo",
"email": "benjamin@acme.dev",
"avatar": null
}
}
]user is the joined account behind the event, and it is null on an event that carries neither a name nor an email address.
history covers one variable in one environment. The same name in staging and production keeps two separate histories, so pass --env per environment you want to look at. For the published versions of a whole environment, see envless version.
Removing
envless var rm plans the whole run before it deletes anything. It looks each name up in each environment, warns about the ones that are not there, then asks once, naming only the variables it actually found and the environments that hold them.
$ envless var rm OLD_TOKEN STRIPE_KEY --env staging
┌ envless var rm
│
✔ 1 variable to remove
⚠ STRIPE_KEY is not set in staging.
│
◆ Remove OLD_TOKEN from staging?
│ ○ Yes / ● No
│
✔ staging → removed OLD_TOKEN
│
└ ✓ Removed 1 variable.The default answer is No. Declining deletes nothing and exits 1. A name missing from an environment is a warning, not a failure, so removing a variable that only exists in staging still exits 0.
Names are collapsed before the lookup, and the lookup itself ignores case, so envless var rm OLD_TOKEN OLD_TOKEN removes one variable and asks about it once, and envless var rm old_token finds a live OLD_TOKEN. With no name at all, rm stops before any network call and exits 2:
$ envless var rm
✗ Pass at least one variable name. Try envless var rm NAME [NAME ...].If no name matched anywhere, the run stops before the prompt and exits 1:
$ envless var rm OLD_TOKEN --env production
┌ envless var rm
│
✖ Nothing to remove.
⚠ OLD_TOKEN is not set in production.
│
└ Aborted.--yes (or -y) skips the confirmation. Without a TTY and without --yes, rm refuses with ✗ Refusing to run unattended. Pass --yes to confirm. and exits 1, so an unattended job can never quietly delete variables.
Removing variables 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 removal once. --yes does not skip the code. Without a TTY the removal 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.
Importing a file
envless var import <file> is a shorthand for envless push with --file pointed at that path. It is the same command underneath, so the diff, the drift check, --prune and --dry-run all behave identically.
envless var import .env.production --env production
envless var import .env.production --env production --prune --dry-runWith no path, import behaves exactly like a plain envless push and looks for .env.<slug> then .env per environment.
Flags
| Flag | Used by | Description |
|---|---|---|
--env <slugs> | all | Environment slug or unique ID. Comma-separated for several on set and rm, single only on get, list and history. Falls back to ENVLESS_ENV, then the environments array in .envless, then a picker on a TTY. Because the fallback is the whole array, get, list and history need this flag in a directory linked to more than one environment. |
--type <type> | set | string, number or boolean. Defaults to string on create, untouched on update. |
--visibility <v> | set | server or client. Defaults to server on create, untouched on update. |
--secret | set | Store the value as a secret. Already the default on create. |
--no-secret | set | Store the value in the clear. Cannot be combined with --secret. |
--required | set | Mark the variable as required. |
--no-required | set | Mark the variable as not required. Cannot be combined with --required. |
--default <value> | set | Store an encrypted default value alongside the value. |
--stdin | set | Read one value from a pipe instead of the command line. Pass exactly one name without a value. |
--reveal | list | Print decrypted values instead of ••••••••. Also adds value to the --json output. |
--json | get, list, history | Machine readable output. On history it prints the raw event objects. |
--yes, -y | rm | Skip the confirmation. Required without a TTY. |
--file <path> | import | The file to push, same as the positional path. |
--workspace <ref> | all | Workspace slug or unique ID. Falls back to .envless, then to the default set by envless workspace use. |
--product <ref> | all | Product slug or unique ID. Falls back to ENVLESS_PRODUCT, then .envless. Only needed when the project slug exists in more than one product. |
--project <ref> | all | Project slug or unique ID. Falls back to ENVLESS_PROJECT, then .envless, then a picker on a TTY. |
Requirements
Every subcommand resolves the same target and needs the same credentials, reads included:
| Requirement | How to satisfy it |
|---|---|
| A signed-in user token | envless login. An ev_sk_ machine key is rejected. |
| A workspace | --workspace, a .envless file, or the default set by envless workspace use. |
| A project | --project, ENVLESS_PROJECT, .envless, or the picker on a TTY. |
| At least one environment | --env, ENVLESS_ENV, .envless, or the picker on a TTY. |
| An unlocked workspace key | envless passphrase set, or ENVLESS_KEY / ENVLESS_PASSPHRASE. See Passphrase. Not needed by history, nor by a metadata-only set. |
Machine keys are read only, and var rejects them even for get and list. 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 exits 1. The token, workspace, key and project failures are reported before the banner. set and rm open the banner before they resolve environments, so an environment failure is printed underneath it instead:
| Situation | Message |
|---|---|
| Not signed in | Not signed in. Run envless login first. |
Machine key in ENVLESS_TOKEN | Machine keys are read only. Run envless login to write from this machine. |
| No workspace anywhere | Missing --workspace. Pass it inline, run envless link, or pick a default with envless workspace use <slug|id>. |
| Workspace key not cached | <workspace> is locked. Run envless passphrase set, or set ENVLESS_KEY. |
| No project, no TTY | Missing --project. Pass it inline or run envless link first. |
| No environment, no TTY | Missing --env. Pass it inline or run envless link first. |
| Environment slug not in the project | Environment <slug> not found in this project. |
Several environments on get, list or history | This command takes a single environment, but .envless links <n> environments. Pass --env <slug> to pick one. The middle clause reads <n> environments were named when the list came from --env or ENVLESS_ENV. |
After that, failures are per subcommand:
| Situation | Message |
|---|---|
A name with no value, no TTY, no --stdin | No value for <name>. Pass <name>=value, or pipe the value in with --stdin. |
--stdin with more or fewer than one bare name | Pass exactly one name without a value when you use --stdin. |
--stdin with an empty pipe | Nothing arrived on stdin for <name>. |
| Fetch failed | Could not load variables (<reason>). or Could not load variables in <env> (<reason>). |
get cannot decrypt the value, or list cannot decrypt any of them | Could not decrypt <name>. Run envless passphrase set to unlock this workspace. |
list cannot decrypt some of them | <n> of <m> variables could not be decrypted and were left out: <names>. Check your passphrase or ENVLESS_KEY. as a warning, the table still prints |
get on a name that is not set | <name> is not set in <env>. Run envless var list to see what is. |
history on a name that is not set | <name> is not set in <env>. |
history could not be read | Could not load history (<reason>). |
| A write was rejected | Could not create <name> in <env> (<reason>). or Could not update <name> in <env> (<reason>). |
rm matched nothing | Nothing to remove. then Aborted. |
rm name missing in one environment | <name> is not set in <env>. as a warning, the run continues |
| A delete was rejected | Could not remove from <env> (<reason>). |
rm unattended without --yes | Refusing to run unattended. Pass --yes to confirm. |
Exit codes
| Code | When |
|---|---|
0 | Every write landed, the value was printed, the table was printed, history had nothing to show yet, or bare envless var printed its usage. |
1 | The target could not be resolved, the workspace is locked, set could not resolve a value (no TTY to prompt on, --stdin with more or fewer than one bare name, an empty pipe, a piped value over the ceiling, or a cancelled prompt), a fetch, decrypt, write, delete or history read failed, get or history found no such variable, rm matched nothing, or the confirmation was declined or refused. |
2 | A usage problem caught before any network call: an unknown subcommand, a bad NAME=value pair, an invalid name, a duplicate name, an oversized name or inline value, an unknown --type or --visibility, conflicting --secret or --required flags, a missing name on set, get, history or rm. |