Manage

Access

List, grant and revoke the workspace members who reach a private product from the CLI.

Usage

bash
envless access list [--product <p>] [--workspace <w>] [--json]
envless access grant <email> [<email> ...] [--product <p>] [--workspace <w>]
envless access revoke <email> [<email> ...] [--product <p>] [--workspace <w>] [--yes]

access decides who reaches one product, and through it every project inside that product. It only bites on a private product, because a product that is not private is open to every member of the workspace. Make it private with envless product update <slug> --private.

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

People are named by email, and every email has to belong to an active member of the workspace already, which is what envless member is for. A role that carries products:manage_private reaches every product whether it was granted or not, so the list is never the whole story. See envless role.

No variable is read or written here, so no passphrase and no workspace key is involved, and neither --project nor --env is used. Every subcommand works on a single product in a single workspace.

Running envless access 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

SubcommandAlso acceptedTargets
listlsEveryone granted the product
grantAs many emails as you pass
revokeAs many emails as you pass

grant and revoke take a list of emails. Each one is trimmed, lower-cased and collapsed before anything happens, so envless access grant Ann@Acme.com ann@acme.com covers one person once, and the match against the team is case-insensitive. Every email in the list is resolved against the workspace team and the current access list before the first write, so one address that does not resolve stops the run before anything changes. revoke then asks once for the whole set.

rm, remove and del are aliases for delete everywhere in the CLI, and access has no delete. envless access rm bob@acme.com prints ✗ Unknown subcommand: rm, then the usage block, and exits 2. The subcommand you want is revoke.

List

bash
envless access list
envless access list --product billing-platform
envless access list --json

Plain output, with no intro block and no spinner, so it pipes cleanly into grep, head, or anything else. The line above the table says whether the product is private, because that is what decides whether the list below matters.

bash
$ envless access list --product billing-platform
› Billing Platform is private, so only the people below reach it, plus anyone whose role can manage private products.
EMAIL         NAME
ann@acme.com  Ann Roy
bob@acme.com  Bob Chen

On a product that is not private the same list is printed under a different line, so a grant you made ahead of time is still visible:

bash
$ envless access list --product marketing-site
› Marketing Site is not private, so every member of Acme reaches it already. The list below starts to matter once you run envless product update marketing-site --private.
EMAIL         NAME
ann@acme.com  Ann Roy

The table is built by joining the granted user IDs against the workspace team, so EMAIL and NAME come from the team rather than from the access record. Rows are sorted by email. A granted user with no team row left to join against has no email to print, so that row falls back to the raw user ID, dimmed, and sorts last. NAME shows - when the team row carries no name.

Nobody granted yet is not a failure. The table view prints a hint and exits 0, so a fresh private product does not fail a script:

bash
$ envless access list --product billing-platform
› Billing Platform is private, so only the people below reach it, plus anyone whose role can manage private products.
› Nobody has been granted access. Add someone with envless access grant <email>.

--json prints one object and nothing else, with no privacy line and no hint. isPrivate and userIds are what the API returned, and members is the same set resolved against the team:

bash
$ envless access list --product billing-platform --json
{
  "isPrivate": true,
  "userIds": [
    "0192a1b2-c3d4-7000-8000-000000000021",
    "0192a1b2-c3d4-7000-8000-000000000022"
  ],
  "members": [
    {
      "userId": "0192a1b2-c3d4-7000-8000-000000000021",
      "email": "ann@acme.com",
      "name": "Ann Roy"
    },
    {
      "userId": "0192a1b2-c3d4-7000-8000-000000000022",
      "email": "bob@acme.com",
      "name": "Bob Chen"
    }
  ]
}

members is sorted by email with the unresolved IDs last, and email and name are null on a user ID with no team row. An empty access list prints an empty members array and still exits 0.

Once the product has resolved, two more lookups have to succeed, the access list and the team. A failure on either prints ✗ Could not load the access list (<error>). or ✗ Could not load the team (<error>). and exits 1. The product is resolved before them, against the product list, and a failure there prints ✗ Could not load products (<error>). and also exits 1.

Grant

bash
envless access grant ann@acme.com
envless access grant ann@acme.com bob@acme.com --product billing-platform

Grant lets the named members reach the product and every project inside it. There is no confirmation, because nothing is taken away.

bash
$ envless access grant ann@acme.com bob@acme.com carol@acme.com
┌  envless access grant
✔ ann@acme.com, bob@acme.com, carol@acme.com
  had access already carol@acme.com
✔ ann@acme.com can reach Billing Platform
✔ bob@acme.com can reach Billing Platform
│
└  ✓ ann@acme.com, bob@acme.com can reach Billing Platform and every project inside it.

An email that already holds access is not sent to the API. It is reported as had access already <email> and the rest are granted. When every email already had it, nothing is called at all and the run still exits 0:

bash
$ envless access grant carol@acme.com
┌  envless access grant
✔ carol@acme.com
  had access already carol@acme.com
│
└  Nothing to do, they all had access already.

Every email has to match an active member of the workspace, which rules out an unknown address and a pending invite alike. The whole set is resolved first, so a single miss grants nothing at all. The reasons are joined into one line, closed with Nothing was granted., followed by Aborted., and the run exits 1:

bash
✗ Not in Acme: dave@acme.com. Invite them with envless member invite <email> first. Nothing was granted.
✗ Still on a pending invite: erin@acme.com. They can be granted access once they accept it. Nothing was granted.

A pending invite cannot be granted, because there is no user account to attach the grant to yet. Grant it once the invite is accepted. envless member list shows who is still pending.

Once the set has resolved, grants are made one at a time and a refusal on one email never stops the rest. The outro counts what happened and the exit code is 1 when anything failed:

bash
$ envless access grant ann@acme.com bob@acme.com
┌  envless access grant
✔ ann@acme.com, bob@acme.com
✔ ann@acme.com can reach Billing Platform
✗ bob@acme.com was skipped (Could not grant product access.)
│
└  ! 1 granted, 1 failed.

When every grant failed there is no count. The run ends on Aborted. and exits 1.

Granting on a product that is not private is allowed and succeeds. The CLI does not pre-judge it, it grants and then says the change is inert for now:

bash
└  ✓ ann@acme.com can reach Marketing Site and every project inside it. Marketing Site is not private, so this changes nothing until you run envless product update marketing-site --private.

Revoke

bash
envless access revoke bob@acme.com
envless access revoke bob@acme.com carol@acme.com --yes
bash
$ envless access revoke bob@acme.com
┌  envless access revoke
✔ bob@acme.com
│
◇  Revoke bob@acme.com from Billing Platform? They lose access to every project inside it.
│  Yes
│
✔ bob@acme.com is off Billing Platform
│
└  ✓ bob@acme.com can no longer reach Billing Platform.

There is one confirmation for the whole set and it names every email 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.

Every email has to be a member of the workspace and has to hold access right now. The whole set is resolved before the confirmation, so a miss stops the run before the prompt and before anything is revoked. The reasons are joined into one line, closed with Nothing was revoked., followed by Aborted., and the run exits 1:

bash
✗ Not in Acme: dave@acme.com. Nothing was revoked.
✗ Never granted access to Billing Platform: carol@acme.com. Nothing was revoked.

A member with no user account behind them can hold no grant, so they are reported as never granted rather than as a separate case.

Revokes run one at a time after the confirmation, and a refusal on one email never stops the rest. The exit code is 1 when anything was kept:

bash
$ envless access revoke ann@acme.com bob@acme.com
┌  envless access revoke
✔ ann@acme.com, bob@acme.com
│
◇  Revoke ann@acme.com, bob@acme.com from Billing Platform? They lose access to every project inside it.
│  Yes
│
✔ ann@acme.com is off Billing Platform
✗ bob@acme.com was kept (Something went wrong while revoking product access.)
│
└  ! 1 revoked, 1 kept.

When every revoke failed the run ends on Aborted. instead of a count, and also exits 1.

Without --yes on a non-TTY, revoke stops before writing with ✗ Refusing to run unattended. Pass --yes to confirm., prints Aborted. and exits 1. That is the flag to add in CI.

Granting or revoking product access 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.

Revoking on a product that is not private also succeeds, and the outro is honest about what it did not achieve:

bash
└  ✓ ann@acme.com can no longer reach Marketing Site. Marketing Site is not private, so they still reach it until you run envless product update marketing-site --private.

Flags

FlagUsed byDescription
--product <ref>allProduct slug or unique ID. Falls back to ENVLESS_PRODUCT, then the product field of .envless, then a picker. A workspace with a single product uses it without asking.
--workspace <ref>allWorkspace slug or unique ID. Falls back to the workspace field of .envless, then the default set by envless workspace use.
--jsonlistPrints isPrivate, userIds and the resolved members as JSON and nothing else.
--yes, -yrevokeSkips the confirmation prompt. Required when output is not a TTY.

grant and list never ask for a confirmation, so --yes does nothing for them.

Requirements

RequirementHow to satisfy it
A signed-in user tokenenvless login. An ev_sk_ machine key is rejected.
A workspace--workspace, a .envless file, or the default set by envless workspace use. There is no environment variable for it.
A product--product, ENVLESS_PRODUCT, .envless, or the picker on a TTY. A workspace with more than one product and no TTY needs the flag.

No passphrase, ENVLESS_KEY, --project or --env is needed, because these commands read and write access records rather than variables. See Passphrase for what does need the key.

The product picker only appears when nothing named a product. It is skipped on a workspace holding exactly one product, which is then used. Cancelling the picker exits 1. On a workspace with several products and no TTY, which is what envless access list --json usually runs on, it asks for the flag rather than blocking on a picker: ✗ Missing --product. Acme has 3 products, so pass one inline or run envless link first. and exits 1. A --product that matches no exact slug or unique ID stops the run with ✗ Product <name> not found in Acme. and exits 1, so a stale product entry in .envless or a stale ENVLESS_PRODUCT is worth clearing with envless link. The product list only carries what your role can see, so a private product you were never granted reads as not found rather than as a permission error.

A workspace with no product you can see has nothing for access to work on, and there is no picker to offer. All three subcommands stop on ✗ No product to manage access for in Acme. Add one with envless product create, or name one with --product. and exit 1. Either the workspace holds no product yet, which envless product create fixes, or every product in it is private and your role was never granted one, which somebody carrying products:manage_private fixes.

All three subcommands run through the write context, so an ev_sk_ machine key is rejected with ✗ Machine keys are read only. Run envless login to write from this machine. even for envless access list. Machine keys are for reading variables, as in envless sync. See Keys.

The API guards all three access endpoints with the products:update permission, and without it the answer is The user is not authorized to manage product access in this workspace. Reading is not the same as changing, though: on a private product grant and revoke also need products:manage_private and refuse with The user is not authorized to manage private products in this workspace., where list carries no such check. Two more permissions come before any of that. Every subcommand resolves the product against the product list, which is guarded by products:read, so a role without it stops on ✗ Could not load products (The user is not authorized to list products in this workspace.). All three then read the workspace team, to turn user IDs into emails and to resolve the emails you passed, and that endpoint is guarded by members:read, so a role without it stops on ✗ Could not load the team (The user is not authorized to list this workspace team.). A role carrying products:update alone reaches none of it. The CLI prints whichever message came back and exits 1 rather than deciding locally. Permissions live on roles, see envless role.

Exit codes

CodeWhen
0Success, an empty access list, a grant where everyone already had access, and bare envless access printing its usage
2Unknown subcommand, grant or revoke with no email, and grant or revoke with a malformed email
1Not signed in, machine key rejected, unresolved workspace or product, cancelled picker, a workspace with no product you can see, a failed product, access or team lookup, an email that does not resolve, a declined or unattended confirmation, and a grant or revoke run where at least one call failed

Errors

SituationBehavior
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 or file✗ 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
Products could not be listed✗ Could not load products (<error>). and exit 1
--product does not resolve✗ Product billing not found in Acme. and exit 1
Several products and no TTY✗ Missing --product. Acme has 3 products, so pass one inline or run envless link first. and exit 1
Workspace with no visible product✗ No product to manage access for in Acme. Add one with envless product create, or name one with --product. and exit 1
Access list could not be read✗ Could not load the access list (<error>). and exit 1
Team could not be read✗ Could not load the team (<error>). and exit 1
grant with no email✗ Missing an email. Run envless access grant <email> [<email> ...], or envless member list to see the team. and exit 2
revoke with no email✗ Missing an email. Run envless access revoke <email> [<email> ...], or envless access list to see who has access. and exit 2
One malformed email✗ <value> is not an email address. Access is granted by email., or revoked by email on revoke, and exit 2
Several malformed emails✗ Not email addresses: <values>. Access is granted by email., or revoked by email on revoke, and exit 2
grant target not in the workspaceNot in Acme: <emails>. Invite them with envless member invite <email> first. Nothing was granted., then Aborted., exit 1
grant target on a pending inviteStill on a pending invite: <emails>. They can be granted access once they accept it. Nothing was granted., then Aborted., exit 1
revoke target not in the workspaceNot in Acme: <emails>. Nothing was revoked., then Aborted., exit 1
revoke target that was never grantedNever granted access to <Product>: <emails>. Nothing was revoked., then Aborted., exit 1
revoke declinedPrints Aborted. and exits 1, nothing is revoked
revoke unattended without --yes✗ Refusing to run unattended. Pass --yes to confirm., then Aborted., exit 1
Private product without products:manage_privateThe grant or revoke call is refused with The user is not authorized to manage private products in this workspace., exit 1
One call refused in a set<email> was skipped (<error>) on grant or <email> was kept (<error>) on revoke, the rest still run, the outro reads ! 1 granted, 1 failed. or ! 1 revoked, 1 kept., exit 1
One call refused on a single emailPrints the API message on its own, with no count around it, and exits 1
Every call refusedPrints Aborted. and exits 1
API rejects the writePrints the API message as it came back and exits 1