Cua Docs

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 login

The 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-RIGM

Approve 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-browser

The 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.EncryptedKeyring

Option 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-browser

Or install it alongside the CLI in one step:

uv tool install cua-cli --with keyrings.alt --index https://wheels.cua.ai/simple

Credentials 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:

VariableEffect
FLEETS_TOKENFleet workload token. When set, it takes precedence over the interactive session for Fleet operations.
CUA_MCP_PERMISSIONSDefault permission list for cua serve-mcp, overridden by --permissions.
CUA_SANDBOXDefault sandbox for MCP computer tools, overridden by --sandbox.
ANTHROPIC_API_KEYUsed by cua do snapshot and by cua skills record --provider anthropic.
OPENAI_API_KEYUsed by cua skills record --provider openai.
PYTHON_KEYRING_BACKENDRead by the keyring library to select where credentials are stored.
XDG_DATA_HOME, XDG_STATE_HOMEMove ~/.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:

VariableEffect
CUA_API_KEYAPI key for cloud sandboxes.
CUA_BASE_URLBase 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_URLBase URL for the Fleet API. Defaults to https://run.cua.ai.
CUA_TOKEN_URLOAuth token endpoint for client-credentials auth.
CUA_CLIENT_ID, CUA_CLIENT_SECRETOAuth client credentials for Fleet.
FLEETS_TOKENStatic 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-sandbox

GitHub 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 --force

Use 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.