The leed CLI does not use an API key. It holds a session — the same kind of session your browser holds, issued to your account, expiring on its own schedule — and it stores that session in a file on your machine. The installer creates it the first time; this page covers everything after that.
Three separate credentials are involved, and they expire at different times for different reasons. Being “logged in” and being able to git fetch are not the same state, which is the single most common source of confusion here — see three credentials, three lifetimes.
Signing in
leed loginThe CLI walks a device-authorization flow, which means your terminal never sees your password and never opens an input box:
- It prints a verification code, formatted
AB12-CD34. - It opens your browser at the CMS approval page with the code already filled in.
- You confirm the code matches what your terminal shows, and approve.
- The CLI notices within a few seconds and writes the session to disk.
Codes are drawn from an alphabet with no I, L, O, 0 or 1, so there is no character you can misread. The code is valid for five minutes, and the CLI polls every five seconds until it is approved or expires. If it expires, run the command again.
If the CLI cannot open a browser — a remote shell, a container, a machine with no desktop — it prints the URL instead. Open it anywhere you are already signed in to the CMS; the approval does not have to happen on the same machine as the terminal.
sequenceDiagram
autonumber
actor You
participant CLI as leed CLI
participant CMS as Leed CMS
participant Browser
CLI->>CMS: POST /api/auth/device/authorize
CMS-->>CLI: deviceCode, userCode, verificationUri, expiresIn 300, interval 5
CLI-->>You: Your verification code is: AB12-CD34
CLI->>Browser: open the approval page with the code prefilled
loop every 5 seconds, for up to 5 minutes
CLI->>CMS: POST /api/auth/device/token
CMS-->>CLI: 428 authorization_pending
end
You->>Browser: check the code and approve
Browser->>CMS: POST /api/auth/device/approve
CLI->>CMS: POST /api/auth/device/token
CMS-->>CLI: accessToken, expiry, userId, email, name
CLI-->>You: Authenticated as … — session written to credentials.json
The browser half of that exchange is documented from the CMS side in authorizing devices and clients, which is also where you go to see and revoke what you have approved.
Magic-link fallback
leed auth login --email you@example.comThis sends a sign-in link to your inbox and then tells you to run the device flow anyway:
Check your email and click the sign-in link.
After clicking the link, run: leed auth login --browser --env productionOmit the address (leed auth login --email) and the CLI asks for it.
Choosing an environment
Credentials are stored per environment, and every authenticated command resolves one before it does anything. Most people never think about this because the default is right; anyone working against a staging site needs to.
| Source | Example | Wins over | Notes |
|---|---|---|---|
--env flag | leed auth login --env staging | everything | Accepts production, staging or dev. Exists on login and logout only, and is hidden from --help |
ENV_NAME environment variable | ENV_NAME=staging leed auth status | config file, default | Read directly by the same option |
leed.config.json | "environment": "staging" | the default | Searched in the current directory, its parent and its grandparent |
| Default | production | — | leed site data commands refuse this fallback rather than guessing |
That last row matters. push, commit, import, create-page-type and list will not silently assume production. Run one outside a site directory with no ENV_NAME set and you get:
Could not determine the target environment. Run from inside a leed-managed site
directory (leed.config.json) or set ENV_NAME=dev|staging|production.Where credentials live
~/.config/leed/credentials.jsonThe directory is created 0700 and the file 0600 — owner-only, both. Set XDG_CONFIG_HOME and the whole leed/ directory moves with it.
The file is a map keyed by environment, so one file can hold a production session and a staging session side by side without either overwriting the other:
{
"production": {
"accessToken": "<redacted>",
"refreshToken": "",
"tokenExpiresAt": "2026-09-09T12:00:00.000Z",
"email": "you@example.com",
"userId": "usr_…"
},
"staging": { /* … */ }
}refreshToken is reserved and always empty today; refreshing works off the access token itself.
One quiet behavior worth knowing: a malformed credentials file parses to an empty map rather than raising an error, so a truncated or half-written file reads exactly like “not logged in”. If leed auth status insists you are signed out on a machine you know you signed in on, look at the file before you look at anything else.
Staying signed in
Sessions last seven days. Two separate mechanisms keep you from noticing.
A proactive refresh runs in the leed site pre-action for push, commit, notify-cms, upload, import, create-page-type and list. If the stored token expires within the next five minutes it is exchanged for a fresh one. If that exchange fails, the CLI only warns — it does not stop the command, because plenty of what follows may not need the token at all.
A hard revalidation runs before every push, and it is not the same thing. It round-trips the refresh endpoint unconditionally, even when the local expiry looks healthy, and if that fails it drops straight into an interactive device login mid-command.
That second one looks paranoid until you consider what it prevents. A session can be revoked server-side — you signed out everywhere, an administrator removed your access — while the copy on your disk still looks perfectly valid. Git would accept the push using its own separate credentials, and then the CMS notification would be refused. Your commits would be on the remote with no deployment created and nothing building, and that notification cannot be replayed by pushing again. Checking first is cheaper than that outcome. This is why leed site push can ask you to log in halfway through.
A refresh works for up to 24 hours past expiry. Inside that grace window a stale session is quietly renewed; past it the server answers token_expired_beyond_grace and you sign in again.
Checking your status
leed auth statusOutside a site directory it lists every environment you hold a session for. Inside one, it also reports the two GitLab tokens that back that site, read live from the CMS with a five-second timeout:
Credentials: /home/you/.config/leed/credentials.json
✓ production: you@example.com (expires Sep 9, 2026)
✓ staging: you@example.com (expires Sep 8, 2026)
GitLab tokens for example.com (staging):
✓ package registry: expires Nov 4, 2026
⚠ raw-content: expires in 5 days (Sep 7, 2026)The credentials path is printed only when a non-production environment is in play — that is exactly when knowing which file to look at is useful.
| Symbol | Meaning | Threshold |
|---|---|---|
| ✓ | Valid | More than 7 days remaining |
| ⚠ | Expiring | 7 days or fewer remaining |
| ✗ | Expired | Past its expiry |
| ? | Unknown | No expiry recorded, or one this CLI cannot parse |
Three failures replace the token rows rather than adding to them. “Session expired. Run: leed auth login --env staging” means the CMS rejected your session. “No developer access for this organization” means you are signed in and simply not allowed — a new login will not help, only an administrator can. Anything else prints “Could not reach …” with the transport error.
If both tokens come back empty you get “(no tokens stored yet — run ‘leed auth rotate’ or rerun the installer)”.
Three credentials, three lifetimes
This is the section that resolves most confusion on this page. “Logged in” describes only the first row.
| Credential | Where it lives | Lifetime | Rotated by | Symptom when stale |
|---|---|---|---|---|
| CMS session token | ~/.config/leed/credentials.json | 7 days, auto-refreshed, 24-hour grace | leed auth login, the automatic refresh | “Session expired”; commands that talk to the CMS refuse |
| GitLab npm registry token | ~/.bunfig.toml, under [install.scopes] | Months; leed auth status warns at 7 days | leed auth rotate, leed auth reset, the installer | CLI updates and bun install fail with a registry authorization error |
| GitLab raw-content token | Embedded in the origin URL in raw-content/.git/config | Months; warned at 7 days | leed auth rotate, leed auth reset, leed site init | git fetch and leed site push fail to authenticate, while leed auth status still shows you signed in |
The two GitLab tokens are written into files described in leed.config.json, files and environment, and the git side of the raw-content token — why a fetch can fail while you are demonstrably signed in — is covered in git workflow.
Rotating and resetting
Two commands, and the difference is whether your CMS session survives.
leed auth rotateRun from inside your site directory. It mints a fresh pair of GitLab tokens for that site, rewrites the @leed entry in ~/.bunfig.toml, and rewrites the repository’s origin URL. It does not touch your CMS session — the command says so when it finishes: “Your CMS login session was not touched. No re-authentication needed.” Rotation is per site, so run it in each site folder if you develop more than one.
Reach for it when leed auth status shows a ⚠ or ✗ on either token row, when git fetch starts failing to authenticate, or as routine hygiene.
leed auth resetThe nuclear option, for a lost laptop or a credential you believe has leaked. It rotates the GitLab tokens first — while the session is still valid, because revoking first would make the rotation fail — then revokes your CMS session and deletes the local credentials for that environment. Add --all-sessions to revoke your sessions on every other device too. Afterwards you must run leed auth login again; the command tells you so.
Flag-level detail for all five auth commands is on leed auth.
Signing out
leed auth logoutA best-effort server-side sign-out followed by deletion of the local credentials for that environment. If the CMS is unreachable the local file is still cleared, so a machine you no longer control cannot keep using a cached token offline. With nothing stored it simply prints “Not logged in to production.” and returns.
CLI sessions appear alongside your browser sessions, tagged CLI or Installer, in sessions, connected accounts and API tokens — which is where you revoke one from the CMS side. A 403 anywhere on this page means your account lacks the developer override rather than a broken session; that is granted per team member, as described in billing and developer access.
leed auth is one of four command groups; the whole tree, with every flag, is indexed at CLI command reference, and the messages any of these commands can produce are decoded in CLI troubleshooting and exit codes.