Authentication
How the cua CLI logs in, where it stores tokens, how to authenticate on a headless host, and which environment variables take effect.
cua authenticates with OAuth device authorization. Endpoints are discovered
from the issuer https://auth.cua.ai/realms/cyclops-cs; authenticated cloud
requests then go to https://run.cua.ai.
Logging in#
cua auth loginThe CLI prints a verification URL and a user code, tries to open a browser, and
then polls until you approve. Because the code is printed, the flow works over
SSH and in terminals with no browser at all — pass --no-browser to skip the
browser attempt entirely:
$ cua auth login --no-browser
Open this URL in any browser:
https://auth.cua.ai/realms/cyclops-cs/device?user_code=QYNE-RIGM
Enter this code if prompted: QYNE-RIGMApprove in any browser, on any machine, then return to the terminal. Check the result at any time:
$ cua auth status
Logged in to run.cua.ai. Access token expires 2026-08-12T22:36:31.604247+00:00.Access tokens are refreshed automatically before authenticated requests, so the
expiry shown by cua auth status moves forward on its own. cua auth logout
asks the issuer to revoke the refresh token and removes the local credentials
either way, even if the network is unavailable. The CLI never prints access or
refresh tokens.
Where credentials are stored#
Tokens go into the operating system credential vault through the keyring
library, under service run.cua.ai and account cua-cli. The CLI does not read
an API key from the environment and does not write one to a .env file.
That vault is macOS Keychain, Windows Credential Manager, or — on Linux — a
Secret Service provider such as gnome-keyring or KWallet. A server typically
has none of these.
Headless hosts#
On a Linux host with no keyring backend, keyring falls back to
keyring.backends.fail.Keyring and every credential operation raises:
$ cua auth login --no-browser
Open this URL in any browser:
https://auth.cua.ai/realms/cyclops-cs/device?user_code=BCOP-JOSP
Enter this code if prompted: BCOP-JOSP
Error: Login failed: No secure credential store is available.
Configure an OS keyring before logging in.Two things are worth knowing about this failure. It happens after you have
already approved in the browser, because the store is only written once the
device flow completes — so the approval is wasted and has to be repeated. And it
is not limited to login: cua auth status and every authenticated command
raise the same error, because reading the vault fails the same way writing does.
What is at stake is a refresh token, not just an access token. The stored credential re-mints access tokens on demand, so it stays useful long after any access token it produced has expired, and automatic refresh keeps rewriting it so it never goes stale on its own. Whoever reads it holds your Cua session until it is revoked. Choose the storage backend accordingly, and prefer an encrypted one wherever the host outlives the job.
Option 1: an encrypted file keyring#
The best fit for most headless hosts. keyrings.cryptfile writes to a file like
the other file-backed backends but encrypts the store with a passphrase (Argon2
key derivation, AES-GCM):
pip install keyrings.cryptfile
export PYTHON_KEYRING_BACKEND=keyrings.cryptfile.cryptfile.CryptFileKeyring
cua auth login --no-browserThe store lives at ~/.local/share/python_keyring/cryptfile_pass.cfg, mode
600. Every command that touches credentials prompts once for the passphrase:
$ cua auth status
Please enter password for encrypted keyring:
Logged in to run.cua.ai. Access token expires 2026-08-12T22:36:31.604247+00:00.That prompt is the tradeoff: fine for a host you log into and work on, unworkable for an unattended job.
keyrings.alt also ships an EncryptedKeyring, which stores to
crypted_pass.cfg at mode 600 and prompts the same way. It needs a crypto
library that keyrings.alt does not itself install, so add it explicitly or the
backend fails to load with ModuleNotFoundError: No module named 'Crypto':
pip install keyrings.alt pycryptodome
export PYTHON_KEYRING_BACKEND=keyrings.alt.file.EncryptedKeyringOption 2: a plaintext file keyring, for hosts that are thrown away#
Use this only where the machine is destroyed after the job — a CI runner, an ephemeral VM — and never on anything that persists.
pip install keyrings.alt
export PYTHON_KEYRING_BACKEND=keyrings.alt.file.PlaintextKeyring
cua auth login --no-browserOr install it alongside the CLI in one step:
uv tool install cua-cli --with keyrings.alt --index https://wheels.cua.ai/simpleCredentials then land in ~/.local/share/python_keyring/keyring_pass.cfg,
created with mode 600. That is the file to protect, and the file to delete
when you are finished with a host that outlived its purpose — removing it, or
running cua auth logout, is what actually gets the credential off the disk.
Setting PYTHON_KEYRING_BACKEND is not strictly required — PlaintextKeyring
registers at a higher priority than the failing backend, so keyring selects it
on its own once installed. Set it anyway: it pins the choice, so installing
another backend later cannot silently move where your tokens are kept.
PlaintextKeyring does not encrypt anything. Values are base64-encoded,
which is an encoding and not a protection — anyone who can read the file can
recover the token with one command. File permissions are the only real barrier,
and they do not survive a backup, a snapshot, a stray tar, or another user
with root. Prefer Option 1 whenever the host persists.
Option 3: a real keyring#
On a persistent Linux server, install and unlock a Secret Service provider such
as gnome-keyring and let the default backend find it. This keeps the CLI on
the same storage path it uses on a desktop, at the cost of having to unlock the
keyring for each session — with dbus-run-session or a PAM module, depending on
how the host is administered.
Option 4: do not log in at all#
For CI, skip the interactive session and hand the CLI a workload token instead. See GitHub Actions below.
Environment variables#
The CLI itself reads only these:
| Variable | Effect |
|---|---|
FLEETS_TOKEN | Fleet workload token. When set, it takes precedence over the interactive session for Fleet operations. |
CUA_MCP_PERMISSIONS | Default permission list for cua serve-mcp, overridden by --permissions. |
CUA_SANDBOX | Default sandbox for MCP computer tools, overridden by --sandbox. |
ANTHROPIC_API_KEY | Used by cua do snapshot and by cua skills record --provider anthropic. |
OPENAI_API_KEY | Used by cua skills record --provider openai. |
PYTHON_KEYRING_BACKEND | Read by the keyring library to select where credentials are stored. |
XDG_DATA_HOME, XDG_STATE_HOME | Move ~/.local/share/cua and ~/.local/state/cua. |
The Sandbox SDK reads a further set. These apply when you import cua_sandbox
in your own code:
| Variable | Effect |
|---|---|
CUA_API_KEY | API key for cloud sandboxes. |
CUA_BASE_URL | Base URL for the older VM API. Its default, https://api.cua.ai, is a retired host — set this explicitly if you use that API. |
CUA_FLEET_BASE_URL | Base URL for the Fleet API. Defaults to https://run.cua.ai. |
CUA_TOKEN_URL | OAuth token endpoint for client-credentials auth. |
CUA_CLIENT_ID, CUA_CLIENT_SECRET | OAuth client credentials for Fleet. |
FLEETS_TOKEN | Static Fleet workload token, checked before client credentials. |
CUA_BASE_URL has no effect on cua commands. The CLI overwrites it at
startup so that cloud requests always go to https://run.cua.ai, whatever the
environment says. Set it only for direct SDK use.
Fleet requests — the --pool path and everything cua wif-token feeds — go to
CUA_FLEET_BASE_URL, which defaults to https://run.cua.ai and is the current
cloud API. CUA_BASE_URL addresses the older VM API instead, and its default
host is retired.
CUA_API_KEY is not a way to skip logging in, and setting it makes things
worse rather than better. The SDK picks its transport with
_uses_fleet(api_key), which is true only when no API key is supplied. So
providing one routes the call away from Fleet and onto the older VM API —
whose host is retired — and it will fail there no matter how valid the key is.
FLEETS_TOKEN is the environment variable that actually reaches a working
API.
CUA_API_KEY is not an authentication path for the CLI either: cua passes its
own session token explicitly on every cloud call, and an explicitly passed key
takes precedence over the environment.
Fleet pools need their own credentials#
cua auth login covers the CLI's own cloud calls, but it does not cover Fleet.
cua sb launch --pool invokes the SDK without passing the interactive session
through, and the SDK resolves Fleet credentials only from FLEETS_TOKEN, or
from CUA_CLIENT_ID and CUA_CLIENT_SECRET. With a perfectly valid session and
none of those exported, the command fails:
$ cua sb launch --pool my-pool --name my-sandbox
Error: Fleet cloud sandboxes require CUA_CLIENT_ID and CUA_CLIENT_SECRET, or cua.configure(client_id=..., client_secret=...).cua auth status still reports a live session at that point, so the message is
easy to misread as an authentication failure. It is not — it is a different
credential for a different API. Export a client id and secret for interactive
use, or use cua wif-token github in CI:
export CUA_CLIENT_ID=...
export CUA_CLIENT_SECRET=...
cua sb launch --pool my-pool --name my-sandboxGitHub Actions#
cua wif-token github exchanges the job's GitHub OIDC identity for a Fleets
token. It runs only inside GitHub Actions, requests the fleets audience, and
prints nothing but the raw token:
$ cua wif-token github
Error: ACTIONS_ID_TOKEN_REQUEST_URL is missing; run in GitHub Actions with permissions: id-token: write.The job needs id-token: write. FLEETS_TOKEN is process-scoped and ephemeral;
while it is set, it takes precedence over any interactive session:
permissions:
id-token: write
contents: read
steps:
- name: Run a non-interactive Fleets sandbox
run: |
export FLEETS_TOKEN="$(cua wif-token github)"
cua sb launch ghcr.io/trycua/mini-swe:latest --name sandbox
cua sb exec sandbox -- pwd
cua sb delete sandbox --forceUse the GitHub-authorized sandbox name sandbox. cua sb delete sandbox --force
releases the claim while preserving the reconciled one-replica pool, template,
and namespace for the next claim.
Under FLEETS_TOKEN, cua sb ls exits with
Listing Fleet sandboxes is not supported; use 'cua sb info NAME'. Address
Fleet sandboxes by name.