Usage
envless member list [--json] [--workspace <w>]
envless member invite <email> [<email> ...] [--role <name|id>] [--workspace <w>]
envless member update <email> --role <name|id> [--workspace <w>]
envless member remove <email> [<email> ...] [--yes] [--workspace <w>]
envless member resend <email> [<email> ...] [--workspace <w>]
envless member leave [--yes] [--workspace <w>]member works on the people in a workspace, one level above products and projects. Nothing here reads or writes a variable, so no passphrase and no workspace key is involved. The roles themselves are managed by envless role, and access to a private product by envless access.
The envless command ships in @goenvless/cli. Install it with npm i -g @goenvless/cli@latest.
Running envless member 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 active member and pending invite in the workspace |
invite | As many email addresses as you pass | |
update | edit | One active member |
remove | rm, del, delete | As many members or pending invites as you pass |
resend | As many pending invites as you pass | |
leave | Only you |
There is no create here, so envless member add prints ✗ Unknown subcommand: add and envless member new prints ✗ Unknown subcommand: new, both exiting 2. The message echoes the word you typed. invite is what adds someone.
Active members and pending invites
member list holds two kinds of row and the STATUS column tells them apart. An active row is someone who accepted and joined. A pending row is an invite that went out and has not been accepted yet. The other subcommands each care which one they got:
| Subcommand | On an active row | On a pending row |
|---|---|---|
update | Moves them to the new role | Refused, remove the invite and send a new one |
remove | Removes the member | Revokes the invite |
resend | Refused, they already joined | Sends the invite email again |
Rows are matched on the email address you pass, trimmed and compared without case, against what the workspace holds right now. There is no matching on name and no partial matching.
List
envless member list
envless member list --jsonPrints one row per person, active rows first and then pending ones, each group sorted by email address. There is no intro block and no spinner, so the output pipes cleanly into grep, head, or anything else. Extra words after list are ignored.
$ envless member list
EMAIL NAME ROLE STATUS
ana@acme.com - Developer active
ben@acme.com - Owner active
lee@acme.com - Viewer pending
sam@acme.com - Developer pendingNAME is filled from a user object on the row and prints - when the row carries none. No row the API returns today carries one, because the account name sits nested under member, which the table does not read, so NAME reads - on every row, active or pending. Use --json and read member.user.name when you need it. Only an active row carries it, since a pending invite has no account behind it yet.
--json prints the rows as the API returned them and nothing else, with no table and no colour. The sort is applied to the table only, so the JSON keeps the order the API sent rather than the active-then-email order above. Each row carries status, uniqueId, email, roleId and a nested role, plus the timestamps and the nested member and invite objects the table leaves out.
An empty team prints [] under --json and a hint in the table view. Both exit 0, so a fresh workspace never fails a script.
$ envless member list
› No one on the team in Acme yet. Invite someone with envless member invite <email>.A failed lookup prints ✗ Could not load the team (<error>). and exits 1. That is also where a permission problem shows up, since reading the team is a permission of its own.
Invite
envless member invite ana@acme.com --role Developer
envless member invite ana@acme.com sam@acme.com --role Developer
envless member invite ana@acme.comEach address gets its own invite email and shows up as a pending row until it is accepted. --role is matched without case against the role name or the role unique ID, so --role developer and the ID envless role list prints both land on Developer.
On a terminal with no --role, the CLI loads the workspace roles and asks which one to use. One run carries one role, so the answer applies to every address in it.
$ envless member invite ana@acme.com
┌ envless member invite
✔ 3 roles to choose from
◇ Invite them as
│ Developer
✔ Invited ana@acme.com to Acme
│
└ ✓ ana@acme.com invited as Developer. They show up as pending until they accept.Addresses are checked on your machine before anything is sent. An address may hold no whitespace, needs exactly one @, needs something in front of it, and needs a dot inside the domain that is neither the first nor the last character. One bad address stops the whole run with ✗ <address> does not look like an email address. Nothing was invited. and exits 2.
Repeats are collapsed without case and the first spelling wins, so envless member invite Ana@acme.com ana@acme.com sends one invite, addressed as Ana@acme.com. The address is sent exactly as you typed it.
Without --role and without a TTY, invite still loads the roles first so the error can name them, then stops with ✖ Missing --role. Roles here: <names> and exits 2. That is the flag to add in CI. A workspace with no roles at all fails with ✖ No roles in Acme to invite into. Add one with envless role create. and exits 1.
Once the role has resolved, invites go out one at a time and a refusal on one address never stops the rest. With several addresses the failing one is named, and with a single address the API message is printed on its own. The exit code is 1 when anything was skipped.
$ envless member invite ana@acme.com sam@acme.com --role Developer
┌ envless member invite
✔ Inviting as Developer
✔ Invited ana@acme.com
✖ sam@acme.com was skipped (This user is already a member of this workspace.)
│
└ ! 1 invited, 1 skipped.When every address was refused, the run ends on Aborted. instead of a count, and also exits 1.
Update
envless member update ana@acme.com --role Admin
envless member edit ana@acme.com --role AdminUpdate takes one address and requires --role, because a role lands on one person and there is nothing else to change. A second address is refused with ✗ Update takes one email at a time. Run it again for <address>. and no --role with ✗ Nothing to update. Pass --role <name|id>, and envless role list shows what is available. Both exit 2 before any request goes out.
$ envless member update ana@acme.com --role Admin
┌ envless member update
✔ ana@acme.com (Developer)
✔ Admin
✔ Role updated in Acme
│
└ ✓ ana@acme.com now has the Admin role.The member is resolved from the team first, with their current role printed next to the address, then the role is resolved from the workspace roles. A member who already holds that role is not written at all. The run prints ana@acme.com already has the Admin role, nothing changed. and exits 0.
A pending invite cannot be moved to another role, since there is no membership to update yet. Revoke it with remove and invite the address again with the role you want.
$ envless member update sam@acme.com --role Admin
┌ envless member update
✖ sam@acme.com has not accepted the invite yet, so the role cannot change. Run envless member remove sam@acme.com and invite them again with the role you want.
└ Aborted.Remove
envless member remove sam@acme.com
envless member remove sam@acme.com lee@acme.com --yes
envless member rm sam@acme.comRemove is the one subcommand that handles both kinds of row. An active member is removed and a pending invite is revoked, and the summary counts the two separately. A pending target is marked as such on the line that resolves the set, so you can see what you are about to do.
$ envless member remove sam@acme.com lee@acme.com
┌ envless member remove
✔ sam@acme.com, lee@acme.com (pending)
◇ Remove 2 people from Acme (sam@acme.com, lee@acme.com)? Removing a member revokes their access to every project in the workspace.
│ Yes
✔ sam@acme.com removed
✔ lee@acme.com invite revoked
│
└ ✓ 1 removed, 1 invite revoked.Every address is resolved from one team lookup before the confirmation, so an address that matches nothing stops the run before anything changes, with <address> is not on the team in Acme. Nothing was removed. for one and Not on the team in Acme: <addresses>. Nothing was removed. for several. Both exit 1.
There is one confirmation for the whole set and it names everyone 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.
Removals run one at a time after the confirmation, and a refusal on one person never stops the rest. With several addresses the one that stayed is named with <address> was kept (<error>), and with a single address the API message is printed on its own. The outro counts what happened, and the exit code is 1 when anything was kept. When every target was kept, the run ends on Aborted. and also exits 1.
Without --yes on a non-TTY the command stops before removing anything with ✗ Refusing to run unattended. Pass --yes to confirm. and exits 1. That is the flag to add in CI.
Inviting, updating or removing a member 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 write once. --yes does not skip the code. Without a TTY the write 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.
Resend
envless member resend sam@acme.com
envless member resend sam@acme.com lee@acme.comResend sends the invite email again for a pending row. There is no confirmation, because nothing is removed or overwritten.
$ envless member resend lee@acme.com
┌ envless member resend
✔ lee@acme.com
✔ lee@acme.com invite sent again
│
└ ✓ lee@acme.com invited again. They stay pending until they accept.Every address has to resolve to a pending invite. One that is not on the team at all fails with <address> has no invite in Acme. Nothing was sent. for one and No invite in Acme for: <addresses>. Nothing was sent. for several. Someone who already joined fails with <address> has already joined, so there is no invite to resend. Nothing was sent. for one and These have already joined, so there is no invite to resend: <addresses>. Nothing was sent. for several. Both checks cover the whole set before the first email goes out, so a single bad address means nothing is sent at all, and both exit 1.
$ envless member resend ana@acme.com lee@acme.com
┌ envless member resend
✖ ana@acme.com has already joined, so there is no invite to resend. Nothing was sent.
└ Aborted.Once the set has resolved, emails go out one at a time and a refusal on one invite never stops the rest. With several addresses the failing one is named with <address> was skipped (<error>), and with a single address the API message is printed on its own. The outro counts what got through and the exit code is 1 when anything was skipped. When every invite was refused, the run ends on Aborted. instead of a count, and also exits 1.
A resolved address is still not the same as a sent email. Nothing caps or cools down resending on the server, so an invite whose email went missing can be sent again straight away, as many times as you need. What can still refuse one invite after the set has resolved is that invite's own state, covered below, or a failed handoff to the mail provider, which arrives as the skip We could not send the invitation email. Please try again. and leaves the resend count untouched.
resendCount and lastResendAt are still recorded on the invite, but nothing is enforced against them. envless member remove <address> revokes the pending row, and inviting again starts a fresh count with a fresh expiry.
An invite that has already expired cannot be resent at all: it fails with This invitation has expired. Revoke it and invite the address again to send a fresh link. Resending never extends expiresAt, so an invite close to expiry needs revoking and reissuing rather than another email.
Leave
envless member leave
envless member leave --yesLeave takes you out of the workspace that --workspace names, the one .envless binds, or the default set by envless workspace use, in that order. It takes no address, and passing one is refused with ✗ Leave takes no email, it only removes you. Run envless member remove <email> to take someone else off the team. and exits 2.
$ envless member leave
┌ envless member leave
◇ Leave Acme? You lose access to every product, project and variable in it, and only a workspace owner can invite you back.
│ Yes
✔ You are out of Acme
│
└ ✓ Left Acme. Run envless workspace use <slug> to pick another workspace. Any .envless in this directory still points at Acme, which you can no longer read.The default answer is No. Declining prints Aborted. and exits 1, and --yes or -y skips the prompt. Without a TTY and without --yes it refuses with ✗ Refusing to run unattended. Pass --yes to confirm. and exits 1.
Nothing picks a new workspace for you afterwards, which is why the outro points at envless workspace use, and why it warns that the .envless file in the directory still names the workspace you just left.
Several people at once
invite, remove and resend take as many addresses as you pass. update takes exactly one, and leave takes none. The three that take a list behave the same way:
- Repeated addresses are collapsed without case before anything runs, so one person is only touched once.
removeandresendresolve every address from a single team lookup up front. If one address matches nothing, or is the wrong kind of row, the run stops there and nothing has changed yet.removeasks its confirmation once for the whole set, naming everyone in it.- After that the API is called once per person, in order. A refusal on one never stops the rest, the outro counts what landed, and the exit code is
1when anything did not.
What the API decides
The CLI checks the shape of what you typed and the status of the row it found. The rest is the API's call, and the CLI prints the message that came back rather than pre-judging it.
| Message | When |
|---|---|
Your current plan does not include team members. Upgrade your plan to invite members. | Inviting on a plan whose member limit is 1, which is Free and Dev. Team seats 15 and Enterprise is unlimited. |
You have reached the maximum of <n> members for your plan. | The limit counts active members and pending invites together. |
This user is already a member of this workspace. | Inviting someone who has already joined |
An invite for this email is already pending. | Inviting an address that already has an invite out. Use resend instead. |
You cannot invite yourself to the workspace. | Inviting your own address |
The owner role cannot be assigned via invite. | The Owner role is a workspace role, so it shows up in the picker and in --role, and is refused. |
You cannot change your own role. | update on your own address |
The workspace owner role cannot be changed. | update on the workspace owner |
The owner role cannot be assigned to another member. | update with the Owner role |
You cannot remove yourself from the workspace. | remove on your own address. Use leave instead. |
The workspace owner cannot be removed. | remove on the workspace owner |
Cannot leave workspace as owner! | leave while you are the workspace owner |
Role not found in this workspace. | The role matched locally but not server side |
The user is not authorized to invite members to this workspace. | Your role lacks that permission. Reading, inviting, updating, removing, revoking and resending each have their own. |
Flags
| Flag | Applies to | Description |
|---|---|---|
--json | list | Prints the rows as the API returned them and nothing else. No table, no colour, no hint on an empty team. |
--role <name|id> | invite, update | Role name or unique ID, matched without case. Asked for on a TTY when invite leaves it out, required on update. |
--yes, -y | remove, leave | Skips the confirmation. Required when output is not a TTY. |
--workspace <w> | all six | Workspace slug or unique ID. Falls back to the workspace field of .envless, then to the default set by envless workspace use. |
--role reads its value from the next argument, so use the --role=<value> form when the name starts with a dash. --role with nothing usable after it counts as not passed, which means a prompt on invite and a usage error on update.
Requirements
All six subcommands go through the same write context, reads included:
| 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 rejected. |
| A workspace | --workspace <slug|id> first, the workspace field of .envless second, and the default set by envless workspace use third. There is no ENVLESS_WORKSPACE. |
No passphrase, ENVLESS_KEY, --project or --env is needed, because these commands read and write membership records rather than variables. With no workspace from any of the three 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.
Machine keys are read only, so an ev_sk_ token in ENVLESS_TOKEN is rejected with ✗ Machine keys are read only. Run envless login to write from this machine. even for envless member list. Machine keys stay useful for reading variables, as in envless sync. See Keys.
The write context resolves a product whenever one is named, and no member subcommand uses it. So a stale product entry in .envless, a stale ENVLESS_PRODUCT, or a --product flag pointing at something deleted fails any envless member subcommand on that lookup with ✗ Product <name> not found in Acme. Clear the value or run envless link again.
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 |
| Team lookup failed | ✗ Could not load the team (<error>). on list, and the same message on a failed spinner then Aborted. elsewhere, exit 1 |
| Roles lookup failed | Could not load roles (<error>). then Aborted. and exit 1 |
invite with no email | ✗ Missing an email. Run envless member invite <email> --role <name>. and exit 2 |
invite with a malformed email | ✗ <address> does not look like an email address. Nothing was invited. and exit 2 |
invite without --role on a non-TTY | ✖ Missing --role. Roles here: <names> then Aborted. and exit 2 |
invite in a workspace with no roles | ✖ No roles in Acme to invite into. Add one with envless role create. then Aborted. and exit 1 |
--role matches nothing | No role matches <ref>. Roles here: <names> then Aborted. and exit 1, and on update in a workspace with no roles at all, No role matches <ref>. with no tail |
| Role picker cancelled | Aborted. and exit 1, nothing is invited |
invite refused on one address | <address> was skipped (<error>) with several addresses and the API message on its own with one, the rest still run, the outro reads ! 1 invited, 1 skipped., exit 1. When every address was refused there is no outro, just Aborted. |
update with no email | ✗ Missing the member to update. Run envless member update <email> --role <name>. and exit 2 |
update with a second email | ✗ Update takes one email at a time. Run it again for <address>. and exit 2 |
update with no --role | ✗ Nothing to update. Pass --role <name|id>, and envless role list shows what is available. and exit 2 |
update on someone not on the team | <address> is not on the team in Acme. Run envless member list to see who is. then Aborted., exit 1 |
update on a pending invite | <address> has not accepted the invite yet, so the role cannot change. plus the remove hint, then Aborted., exit 1 |
remove with no email | ✗ Missing an email. Run envless member remove <email>, or envless member list to see the team. and exit 2 |
remove target not on the team | <address> is not on the team in Acme. Nothing was removed., or Not on the team in Acme: <addresses>. Nothing was removed., then Aborted., exit 1 |
remove declined | Aborted. and exit 1, nothing is removed |
remove refused on one person | <address> was kept (<error>) with several addresses and the API message on its own with one, the rest still run, the outro reads ! 1 removed, 1 kept., exit 1 |
resend with no email | ✗ Missing an email. Run envless member resend <email>, or envless member list to see the pending invites. and exit 2 |
resend target has no invite | <address> has no invite in Acme. Nothing was sent., or No invite in Acme for: <addresses>. Nothing was sent., then Aborted., exit 1 |
resend target already joined | <address> has already joined, so there is no invite to resend. Nothing was sent., or These have already joined, so there is no invite to resend: <addresses>. Nothing was sent., then Aborted., exit 1 |
resend refused on one invite | <address> was skipped (<error>) with several addresses and the API message on its own with one, the rest still run, the outro reads ! 1 sent, 1 skipped., exit 1. When every invite was refused there is no outro, just Aborted. |
leave with an email | ✗ Leave takes no email, it only removes you. Run envless member remove <email> to take someone else off the team. and exit 2 |
Confirmation unattended without --yes | ✗ Refusing to run unattended. Pass --yes to confirm. and exit 1 |
| API rejects a write | Prints the API message as it came back and exits 1 |
Exit codes
| Code | When |
|---|---|
0 | A run that landed, list including an empty team, an update where the member already holds that role, and bare envless member printing its usage |
2 | Unknown subcommand, a missing email on invite, update, remove or resend, a malformed email on invite, a second email on update, a missing --role on update, invite without --role on a non-TTY, and any word passed to leave |
1 | Not signed in, machine key rejected, unresolved workspace or product, a failed team or roles lookup, a target that does not resolve or is the wrong kind of row, a --role that matches nothing, a cancelled picker, a declined or unattended confirmation, any API refusal, and a multi-target run where at least one target failed |