Every envless login mints one CLI token and writes it to that machine. The dashboard lists every device still holding one on the CLI tokens page, reachable from the user menu at the bottom of the sidebar. Revoking a row there is the kill switch for a laptop you no longer have.
The envless command ships in @goenvless/cli. Install it with npm i -g @goenvless/cli@latest.
A lost or stolen device
Open the CLI tokens page
Go to https://envless.cloud/dashboard/clis. The list holds every session opened by your own account, on any machine.
Identify the device
Match on hostname first. If several machines share a name, sort by Last used and read the IP address printed under the timestamp.
Revoke it
Open the row menu at the end of the line and choose Revoke. The dialog asks to confirm and nothing else, so it takes two clicks. The token stops authenticating the moment the write lands.
Rotate the workspace passphrase
Do this from the dashboard once the session is dead. Revoking removes the device's access to the API, but it cannot reach onto that disk, and the disk is where the decryption material sits. See Choosing a passphrase.
Revoking kills the token, not the plaintext already on the machine. envless sync writes decrypted .env.<slug> files into every project directory it was run in, and envless passphrase set caches the derived workspace key in ~/.envless/config.json. Both survive a revoke. That cached key also decrypts any virtual env feed the workspace exposes, and those URLs are public and need no token at all. Treat a stolen laptop as a passphrase rotation, not just a revoke.
A CLI session is not scoped to a workspace
This is the part worth reading twice. When you approve a device code, the token row records the workspace you had selected in the dashboard at that moment. No authorization path ever reads it back. Every dashboard endpoint takes the workspace from the request body, falling back to whichever workspace your account currently has selected when the body omits it, and checks your membership and permissions against that workspace, so a CLI token acts with your full identity in every workspace you belong to, not just the one recorded on it.
The recorded workspace has two observable effects, and neither of them is a permission boundary. It decides which workspace's webhook endpoints receive the cli_token.created and cli_token.revoked events for that session, and it decides whether the session survives a membership change: removing that user from that workspace, or deleting that workspace, revokes the token outright. A session recorded on any other workspace is untouched by both.
Switching workspace does not contain a lost device. Neither envless workspace use nor changing the workspace in the dashboard narrows what a stolen token can reach, because nothing on the token was limiting it in the first place. Revoke the session. That is the only action that takes anything away from it.
This is the opposite of a personal access key. An ev_sk_ key carries its own workspace, and the API resolves the workspace from the key itself, so a leaked key is confined to one workspace. A CLI token is not. See Keys.
The device table
| Column | What it shows |
|---|---|
| Hostname | The machine name the CLI reported, or Unknown when it reported nothing |
| OS | darwin, linux and win32 are relabeled macOS, Linux and Windows, with the kernel release appended when it is known |
| Arch | The CPU architecture, such as arm64 or x64 |
| CLI version | The version of @goenvless/cli that opened the session |
| Last used | Relative time of the most recent authenticated request, with the client IP underneath. Reads Never until the session makes its first call |
| Created | When the device flow completed and the token was issued |
Everything in the first four columns is self reported by the CLI at login time. It comes from os.hostname(), process.platform, os.release(), process.arch and the installed package version, is truncated at 255 characters, and is never verified. It is also captured once and never refreshed, so a machine you rename or a CLI you upgrade keeps showing what it reported the day it logged in. Read the metadata as a label, not as proof of which machine is calling.
Last used is the column that is live. Every authenticated request stamps lastUsedAt and the caller's IP, taken from X-Forwarded-For (first entry), then CF-Connecting-IP, then X-Real-IP.
The search box filters on hostname, OS, OS version, architecture and CLI version. Sorting is by hostname (the default), last used or created. The table shows 15 rows per page, and the rows per page selector (15, 30, 60, 100, all items) only appears once you have more than 15 sessions.
The page is per user. It lists your sessions only, and there is no workspace wide view of other members' devices. Owners and admins have no path to revoke someone else's CLI session directly, from the dashboard or the API: the revoke matches on the token id and your user id, and a token belonging to anyone else comes back 404 with Token not found or you do not have permission to revoke it. If a teammate loses a laptop, they have to revoke it themselves. Removing them from the workspace is the blunt fallback, and it is not the same thing: it revokes every session whose recorded workspace is the one you removed them from, which kills those tokens everywhere rather than only in that workspace, while any session they approved from another workspace survives and keeps working. No cli_token.revoked event fires on that path.
Revoking
Revoke is the only action on a row. It marks the token deleted server side, with a timestamp, and drops it off the list. Nothing is pushed to the device: it keeps its copy of a token that no longer matches anything, and finds out on its next command, which fails with 401 and auth.cli_token_not_found. Reconnecting means running envless login again, which mints a new token and a new row.
envless logout does the same revoke for the machine you are sitting at, and also clears the local credentials that a dashboard revoke cannot touch. Use the dashboard for machines you cannot type on.
There is no revoke all button. Sessions come off one row at a time.
Session lifetime
A CLI token expires 90 days after it is issued. There is no refresh and no sliding window: using the CLI daily does not extend it, it only updates the last used stamp. Once past the expiry, every request fails with 401 and auth.cli_token_expired, and the fix is to run envless login again.
Two consequences worth knowing:
- The table has no expiry column. You can see when a session was created, and add 90 days.
- Nothing sweeps expired tokens. The hourly cleanup job deletes stale device codes, not tokens, so an expired session keeps sitting in the list until you revoke it. Revoking a dead session changes nothing about access and is purely housekeeping.
The short lived code in the login flow is a separate clock: the device code expires 5 minutes after it is issued. See Login.
How the token is stored
The raw token is env_cli_ followed by 32 random bytes in hex. It is returned exactly once, at the end of the device flow, and written to ~/.envless/config.json with mode 0600.
The server keeps a SHA-256 hash of it and a 12 character prefix, never the token itself. Authentication hashes whatever bearer token arrives and looks for a matching row, so a database dump does not yield a working session. This also means the token cannot be shown again: the dashboard displays no part of it, not even the prefix, and the list endpoint does not return one. A CLI token you lose is replaced, never recovered, the same as an ev_sk_ key.
Webhook events
Both ends of a session's life are webhook events, delivered to the workspace recorded on the token. Only the CLI token endpoints emit them, so a token revoked as a side effect of member removal or workspace deletion is silent.
| Event | When it fires |
|---|---|
cli_token.created | A device code flow completes and a token is issued |
cli_token.revoked | A session is revoked, from the dashboard or by envless logout |
cli_token.created carries the token id, the 12 character prefix and the reported device metadata. It never carries the raw token. Its actor is the user who approved the code in the browser, since the CLI doing the polling is unauthenticated. cli_token.revoked carries the token id, plus reason: "logout" when the CLI revoked itself rather than being cut off from the dashboard.
Because the workspace on the token is the one that happened to be selected at approval time, an endpoint in another workspace never sees these events, even though that same token can read and write that workspace's data. If you rely on webhooks to watch new CLI sessions, subscribe in every workspace, and expect the created and revoked events for one device to land in whichever workspace it was approved from.