A signed-in session is enough to read a workspace and to edit a variable. It is not enough to hand out access, destroy a credential, rewrite an environment in one shot, or touch the account itself. 43 actions ask you to prove, right then, that the person driving the session is still you. Envless calls that step-up verification, and it applies to a CLI token exactly as it applies to a browser session.
The proof is one code: an emailed code, or, if you have two factor authentication turned on, a code from your authenticator app. Whichever you use, it buys a one hour window in which the rest of the guarded actions run without asking again. The dialog takes six characters, so it does not accept a recovery code, use the emailed code if your phone is not to hand. The CLI prompt takes either.
What it guards
The list below is the complete one. Every action here is refused with 403 and auth.step_up_required unless a live elevation covers it.
Your account
Seven actions are scoped to the account rather than to a workspace, and they are the ones that change how you sign in or end the account outright.
| Action | Where |
|---|---|
| Turn two factor authentication on | Account |
| Turn two factor authentication off | Account |
| Regenerate your recovery codes | Account |
| Change your account email | Account |
| Connect a sign-in provider | Account |
| Disconnect a sign-in provider | Account |
| Delete your account | Account, Danger zone |
API keys
| Action | Where |
|---|---|
| Create a key | Keys |
| Rotate a key | Key row menu |
| Delete a key | Key row menu |
| Deactivate a key, re-activate one, or change its scopes or project allowlist | Key row menu and the edit modal |
Renaming a key is the one key edit that is not guarded. The condition is on what the edit changes, not on which endpoint you call: active flipping in either direction, or a new scopes object, or a new projectIds list. A request that sends only name runs on a plain session.
Webhooks
| Action | Where |
|---|---|
| Create an endpoint | Webhooks |
| Edit an endpoint, including enable and disable | Endpoint row menu |
| Delete an endpoint | Endpoint row menu |
| Rotate the signing secret | Endpoint row menu |
| Reveal the signing secret | Secret column |
Members, roles and product access
| Action | Where |
|---|---|
| Invite a member | Team |
| Change a member's role | Team |
| Remove a member | Team |
| Create a role | Settings, Roles |
| Edit a role | Settings, Roles |
| Delete a role | Settings, Roles |
| Grant a member access to a private product | Product settings, Access |
| Revoke a member's access to a private product | Product settings, Access |
Products, projects and environments
| Action | Where |
|---|---|
| Update a product, including its name, slug, icon and privacy flag | Product settings |
| Delete a product | Product settings |
| Delete a project | Project settings |
| Delete an environment | Environment settings |
| Expose an environment, or retract it | Environment settings |
| Publish an example link, or retract it | Environment settings |
Variables and versions
| Action | Where |
|---|---|
Apply a raw .env edit | Raw editor, Apply changes |
| Bulk edit the type, secret flag, visibility or required flag of selected variables | Selection bar on Variables |
| Bulk delete selected variables | Selection bar on Variables |
| Roll an environment back to an earlier version | Versions |
Single-variable work is not guarded. Creating, editing or deleting one variable, and importing a block through Add variables, all run on a plain session. What is guarded is the wholesale shape: a raw apply replaces the entire document in one request, a bulk action rewrites every selected row, and a rollback replaces the environment with an older snapshot.
Workspace and encryption
| Action | Where |
|---|---|
| Delete the workspace | Settings, Danger zone |
| Turn Virtual Envs on or off | Settings, Virtual Envs |
| Claim a custom domain | Settings, Virtual Envs |
| Remove a custom domain | Settings, Virtual Envs |
| Require two factor authentication of every member, or stop requiring it | Settings |
| Turn verification codes off for the workspace | Settings, Verification Codes. This one always asks for a new code, see below |
| Claim the passphrase rotation lock | Settings, Encryption |
Billing and the CLI
| Action | Where |
|---|---|
| Open the billing customer portal | Billing |
| Approve a CLI device code | The page envless login opens |
Account scope and workspace scope
Every challenge carries a scope, and the scope decides what it unlocks.
| Scope | Issued when | Satisfies |
|---|---|---|
| Workspace | The request names a workspace, which is every action above except the seven account ones | Guarded actions in that workspace only |
| Account | The request names no workspace | The seven account actions, and any workspace check |
The asymmetry is deliberate. An account elevation is the stronger proof, so it stands in for a workspace one; a workspace elevation does not reach the account. Switch workspaces inside the hour and the next guarded action in the new workspace asks again, unless what you hold is an account elevation.
Account-scoped actions used to be reachable a different way: account deletion checked every workspace you belonged to, and a user who belonged to none skipped the gate entirely. That path is gone. Deletion now needs a real account elevation whether you hold workspaces or not, and requesting one no longer depends on membership anywhere.
The two factors it accepts
Verification accepts a code from either factor, and the dialog decides which one it asks for first.
| Factor | Available to | What the dialog does |
|---|---|---|
| Emailed code | Everyone | Sends a code to the account address as soon as it opens |
| Authenticator app | Accounts with two factor authentication on | Opens on the app-code field and sends no email until you switch |
With two factor authentication on, the dialog opens asking for the app code and offers the emailed code as the alternative underneath. Nothing is mailed unless you pick it, so the ordinary path costs no round trip through your inbox at all. Recovery codes work here too, and each one still burns on use.
Signing in is app-only; step-up is not. At sign-in the second factor has to be the authenticator app, because the first factor there is already a code mailed to the same address and accepting a second one would let a single mailbox pass both checks. Step-up runs against an already authenticated session, where we cannot assume the phone is within reach, so both factors are offered.
How it works
You trigger a guarded action
The API returns 403 with auth.step_up_required and the message This action needs a verification code to continue. rather than performing the action, and the dashboard opens the verification dialog.
You get a code
The emailed code is 6 characters drawn from a 32-symbol alphabet with the ambiguous I, O, 0 and 1 removed, so it reads cleanly. Only a SHA-256 hash of it is stored, and the comparison is constant-time. An app code is the usual 6 digits from your authenticator.
You enter it
An emailed code is valid for 10 minutes and allows 5 attempts. A sixth attempt is rejected outright even if the code is correct, so a wrong-code loop cannot be brute-forced. App codes carry their own limit: 10 accumulated failures lock the app path, the lock lifts 15 minutes after the last attempt, and a code that works clears the counter. Recovery codes are accepted throughout, including while the app path is locked, so a lost phone is never a dead end.
You get a one hour window
A successful verification elevates you for 60 minutes from the moment you verified. It does not slide forward as you work. Every guarded action in scope inside that window runs without another code, so rotating three keys is one round trip, not three.
The original call is retried
The dashboard, and the CLI, rerun the request that was refused, exactly once, as soon as verification succeeds. You do not click the action or run the command again.
Asking again is not throttled. Nothing caps how many codes you can request, so a code that never arrived can be asked for again immediately. Each request issues a fresh challenge and verification always checks the newest live one, which means an earlier code stops working the moment you ask for another. A request made while you are already elevated answers 200 with elevated: true, sent: false and sends nothing, so the dialog can ask without spending a code.
Passphrase rotation is gated at the lock
Rotating the workspace passphrase is guarded when you claim the rotation lock, not on the re-encrypt calls that follow it.
That is not an oversight. One rotation is an unbounded fan-out of re-encrypt requests inside a single held lock, sized by how many variables, versions and history entries the workspace holds. An elevation expiring partway through would abandon the run with the workspace split across two encryption keys. Gating the entry point instead means the check happens once, before anything is rewritten, and the run cannot stall on a code prompt. Releasing the lock is not guarded either, so a rotation you abandon can always be cleaned up.
What is not covered
The public API-key surface enforces no step-up at all. It mirrors most of the actions above, and a key with the right scopes performs them with no challenge, because an ev_sk_ key has no inbox and no authenticator to answer with. Step-up protects a hijacked browser session. It is not a second gate over the REST API, and scoping keys narrowly is still the only control there. See Authentication.
Some neighbours of guarded actions are deliberately open. Leaving a workspace yourself, revoking or resending an invite, resending a webhook delivery, denying a CLI device code, and revoking a CLI session token all run on a plain session. Note the last one in particular: deleting an API key asks for a code, revoking a CLI session does not.
The elevation is not bound to the credential that earned it. It is a row against your user and the scope, so any live session of yours inside the window benefits, a browser tab and a terminal alike. A CLI token can earn a workspace elevation: /internal/step-up/request and /internal/step-up/verify accept a CLI token principal when the request names a workspace. They still refuse one with auth.cli_token_not_accepted at account scope, because an account elevation would unlock the seven account actions and a terminal has no business changing your email address. Account deletion, two factor enrollment and CLI device approval refuse a CLI token outright as before.
From the CLI
Every guarded action the CLI can reach asks for the same code. That covers envless push, envless var rm, envless version restore, the delete subcommands of workspace, product, project and env, envless env expose, envless product update, envless workspace virtual-envs, envless domain claim and envless domain remove, the member, access and role write subcommands. When the server refuses with auth.step_up_required, the command pauses its spinner, requests a code, prompts for it, verifies it, and reruns the refused request once. One code covers the rest of the run and, like the dashboard, the next hour.
✔ Resolving billing
◇ Delete Billing? Its projects, environments and variables go with it.
│ Yes
› Enter the code we emailed you.
◆ Verification code
│ K7M2PX
└
› ✓ Verified. We will not ask again for an hour.
✔ Removed from AcmeWith two factor authentication on, the prompt says so and takes an authenticator code or a recovery code in the same field; the server works out which one it got, and an emailed code still works. The prompt repeats on a wrong code up to five times, matching the server's attempt cap, and stops at once if the authenticator path is locked.
Nothing bypasses the code. --yes skips the CLI's own confirmation and nothing else. Without a terminal to type into, the command fails before a code is even requested, with This action needs a verification code. Run it from an interactive terminal, or do it from the dashboard. A CI job that pushes with a CLI token therefore fails cleanly rather than hanging. Either verify from the dashboard first, which elevates the same user for an hour, or turn verification codes off for that workspace, see below.
Turning it off for a workspace
A workspace can switch the guard off for itself, under Settings, Verification Codes. It is on for every workspace by default, and the verification dialog itself points members who hold workspace:update at the switch; everyone else sees only the code field. With it off, the 36 workspace-scoped actions above run on a signed-in session or a CLI token alone, in the dashboard and in the CLI, and auth.step_up_required is never answered for that workspace. The seven account-scoped actions are not affected; they are yours, not the workspace's, and keep asking.
Turning it off is itself guarded, and more strictly than anything else: it asks for a new code every time, whether or not you verified something a minute ago. The server answers auth.step_up_fresh_required instead of auth.step_up_required, the dialog opens on a fresh challenge, and the retry only passes if a code was verified within the last two minutes. An elevation you already hold does not count. Turning it back on needs the workspace:update permission and nothing else, because that is the safe direction.
Both directions notify. Every member of the workspace, the person who made the change included, gets an email naming who made it, and the audit log records a workspace.updated event with changes: ["requireStepUp"], which reaches any webhook subscribed to it. The email that announces the guard is off says what to do if nobody on the team meant it: turn it back on, review the team list, and rotate whatever the session that did it could reach.
Turning the guard off does not help a workspace whose owner has lost their inbox. Switching it off needs a fresh code, and without an authenticator that code arrives by email. Enroll an authenticator before that becomes a problem, not after.
Operational consequences
Without two factor authentication, whoever revokes a leaked key needs their inbox at that moment. Revocation is guarded, so an account whose email nobody can currently receive cannot revoke its own keys. Turning on two factor authentication removes that dependency, because an app code satisfies step-up on its own. Either way, do not leave production key ownership with a departed employee's account or a shared address nobody monitors.
If an emailed code does not arrive, check the spam folder for the account's own address rather than a team alias, then ask for another. Expired and consumed challenges are cleaned up on a schedule, so an old code never lingers as a valid credential.
Going deeper
Two factor authentication
Enrolling an authenticator app, recovery codes, and what happens at sign-in.
API keys
Creating, scoping, rotating and revoking machine credentials.
Managing webhooks
Endpoints, signing secrets, and replaying a failed delivery.
Account
Email changes, connected providers, and deleting the account.