arkor login, arkor logout, arkor whoami
Three short commands that read or write ~/.arkor/credentials.json. None of them touches .arkor/state.json. For anonymous workspaces, project routing is auto-created by the runtime on the first trainer.start() (or first inference call). For OAuth workspaces, .arkor/state.json has to exist before the run: today, neither arkor login nor arkor init creates it, so the practical path is to create it manually with { orgSlug, projectSlug, projectId }. The runtime points you at this fallback when it errors out on a missing state file.
arkor login
--oauth.
Synopsis
Options
What happens
- The CLI calls
/v1/auth/cli/configon the cloud-api to read the deployment’s OAuth settings. - If
--anonymousis passed, the CLI requests an anonymous token from/v1/auth/anonymousand writes it to~/.arkor/credentials.jsonwithmode: "anon". - If
--oauthis passed, it skips straight to the OAuth flow. - If no flag is passed, it shows an interactive picker (
OAuth (browser)/Anonymous) with Anonymous preselected. Accepting the default, including in non-interactive contexts where the prompt is non-blocking, runs the anonymous path; choosing OAuth runs the OAuth flow. - The OAuth flow generates a PKCE pair, starts a loopback HTTP server on one of the cloud-api-provided callback ports, opens (or prints) the authorize URL, and waits for the callback. State is verified before exchanging the code for tokens; a mismatch aborts to prevent CSRF. The resulting OAuth tokens are written to
~/.arkor/credentials.jsonwithmode: "auth0".
finally block so it does not stick around if login fails.
Anonymous mode in one paragraph
Anonymous credentials let you try Arkor without an account: training runs, jobs, and any work you do are tied to the local machine via the anonymous token. The anonymous path always mints a brand-new token (and a newanonymousId) and overwrites ~/.arkor/credentials.json, so re-running arkor login --anonymous does not refresh the existing identity. Switching to OAuth (arkor login --oauth, or selecting OAuth (browser) in the picker) overwrites the credentials file the same way and does not migrate prior anonymous workspaces or jobs into the account. Merging anonymous work into an OAuth account once you sign in is on the roadmap; until that lands, run arkor login --oauth before you start the runs you want associated with the account.
Anonymous accounts are intentionally single-device: the cloud-api stores a latest_jti per anonymous user and userAuth rejects every authenticated request whose JWT carries a different jti. The jti only changes when the token rotates, so the practical timing has two regimes. Today the CLI does not auto-refresh anonymous tokens (that wiring lives in @arkor/cloud-api-client’s getToken() and is on the SDK roadmap), so no rotation ever happens client-side and a copy of ~/.arkor/credentials.json on a second machine usually keeps working — until the issuing user explicitly mints a new identity (arkor login --anonymous overwrites the file with a fresh jti) or an admin rotates server-side, at which point every other copy starts failing on its next call. Once auto-refresh ships, any refresh rotates latest_jti immediately, so the older-jti copy on any other device (or an older backup of the same file on this machine) starts failing on its next call without warning. Either way, the losing client receives an HTTP 401 / 409 with code: "anonymous_token_single_device", which cli/main.ts surfaces as actionable guidance; deletion of the underlying anonymous row surfaces as code: "anonymous_account_not_found" the same way. The recovery hint is deployment-aware: on OAuth-supporting deployments the CLI points at arkor login --oauth so you can sign up for an account that supports multiple devices, while on anon-only deployments (where OAuth is not configured) it points at arkor login --anonymous instead — --oauth would fail there, and minting a fresh anonymous identity is the only recovery available. Note that neither path migrates existing anonymous work into the new identity; the previous workspace stays reachable only from the credentials file that issued it.
Anonymous issuance output
Both anonymous paths surface the newanonymousId and an explanation that the same id is how Arkor Cloud recognises this client across sessions, though the line shape differs by entry point. arkor login (--anonymous flag or picker → Anonymous) prints Anonymous id: <id> as the spinner stop and then a separate info line saying that keeping the credentials file (credentialsPath(), typically ~/.arkor/credentials.json on Linux and macOS) is what preserves the identity. arkor dev’s auto-bootstrap skips the spinner and emits a single info line that already embeds the id and the same explanation. The picker → Anonymous path additionally surfaces a one-line warn alongside the success message — Anonymous sessions aren't guaranteed to persist — sign in with `arkor login --oauth` to tie future work to your Arkor Cloud account. — so the upgrade hint is visible at issuance time. The explicit --anonymous shortcut suppresses that warn because it skips the /v1/auth/cli/config fetch and so cannot tell whether arkor login --oauth would succeed on this deployment; pointing at it on a rare anon-only deployment would steer users at a command that fails.
Every anonymous issuance path also surfaces the single-device limitation as a separate info line. The exact wording is gated on oauthAvailable (same contract as the persistence nudge). On OAuth-supporting deployments callers see the upgrade-flavoured Note: anonymous accounts work on this machine only. Run `arkor login --oauth` to sign up for multi-device access., while on anon-only deployments (and on arkor login --anonymous where the cfg fetch is skipped intentionally) callers see only the bare Note: anonymous accounts work on this machine only., so users on those deployments aren’t pointed at a command that would fail. arkor whoami on an anonymous identity also emits the bare variant on stderr, and only when both stdout and stderr are TTYs. A redirected stream (lost stdout.isTTY) or a CI runner that treats stderr-on-success as a warning marker (lost stderr.isTTY) sees no extra prose. The note is purely a UX hint; it doesn’t gate any behaviour. The stdout shape (the user JSON object printed by JSON.stringify(user, null, 2), optionally followed by a single human-readable Orgs: <slug>, … line when the user belongs to any orgs) is unchanged from the pre-note behaviour. Don’t pipe the full output through a strict JSON parser like jq; the Orgs: line is a summary tail, not part of the JSON document.
arkor logout
~/.arkor/credentials.json. Prompts for confirmation by default.
Synopsis
Options
Behavior
- If the credentials file does not exist, the CLI logs
No credentials on file.and exits. - If the user declines the prompt, the CLI logs
Aborted.and exits without deleting. arkor logoutdoes not touch.arkor/state.jsonor the.arkor/build/artifact. To start fully fresh, remove.arkor/manually as well.
arkor whoami
/v1/me on the cloud-api, plus the org slugs you can reach.
Synopsis
Output
When signed in, the command prints the JSONuser object pretty-printed, then a single Orgs: <slug>, <slug>, … line if the response includes any. When ~/.arkor/credentials.json is missing, it prints Not signed in. Run \arkor login` or `arkor login —anonymous`.` and exits.
Exit codes
0: signed in, identity printed.0: not signed in; the message is informational only.1: the cloud-api returned426 Upgrade Required. The CLI prints the upgrade hint (and the upgrade command for your detected package manager) and setsprocess.exitCode = 1so the deprecation-warning flush inarkor’s shutdown hook still runs before the process exits.1: the cloud-api returned a structured anonymous-auth dead-end (code: "anonymous_token_single_device"orcode: "anonymous_account_not_found").cli/main.tsformats the deployment-aware recovery hint to stderr; see Token expiry for the exact wording.1: any other non-2xx (transient 5xx, an unmapped 4xx, etc.). The previous “exit0with aFailed to fetch /v1/me (<status>). Token may be expired.line on stdout” behaviour was removed in this release:arkor whoaminow raises aCloudApiError, andbin.tsrenders it concisely aserr.message(e.g.cloud-api 503) on stderr before exiting non-zero. Stack traces are reserved for unknownErrorshapes (likely SDK bugs); routine HTTP failures stay one-line so they don’t drown a wrapper’s logs.
Where the credentials live
Both modes write to the same file at~/.arkor/credentials.json and are tagged with a mode field of either "auth0" or "anon" so the rest of the CLI (and the Studio server) knows which path to use. See Project structure for the full layout.
Token expiry
For OAuth sessions, the credentials file records both the access token and the issued refresh token, plus theexpiresAt timestamp returned by the token exchange. The refresh token is stored today, but the CLI does not yet auto-refresh expired access tokens; that path is on the roadmap.
In practice that means:
-
An expired or revoked token shows up as a non-2xx response from the cloud-api.
arkor whoamino longer prints a generic “Token may be expired” hint; instead it raises aCloudApiErrorcarrying the upstreamcode(when present), and the top-level handler incli/main.tsformats two known auth-state codes as actionable guidance. Before formatting,main()does a best-effort fetch of/v1/auth/cli/configso the recovery hint matches the deployment shape:code: "anonymous_token_single_device"→- on OAuth-supporting deployments:
Anonymous credentials were rejected as single-device. Anonymous accounts only work on one machine. Sign up for an account that supports multiple devices: arkor login --oauth(exit1). - on anon-only deployments:
Anonymous credentials were rejected as single-device. Anonymous accounts only work on one machine. This deployment does not advertise OAuth, so the only recovery is to mint a new anonymous identity (your previous workspace data cannot be recovered): arkor login --anonymous(exit1).
- on OAuth-supporting deployments:
code: "anonymous_account_not_found"→ analogous OAuth-vs-anon-only split, ending inarkor login --oauthorarkor login --anonymous.
code(and any non-CloudApiErrorexceptions) are rethrown out ofmain().bin.tswraps the top-levelawait main(...)in a try/catch that distinguishes by error shape: aCloudApiErroris rendered as justerr.message(e.g. the upstreamerrorbody, orcloud-api <status>when the body was empty), keeping routine HTTP failures one-line and easy to diagnose; any otherErroris logged witherr.stack ?? err.messageso genuine SDK bugs still surface a frame. Either wayprocess.exitCode = 1is set rather than callingprocess.exit(1)so the deprecation-warning flush + telemetry shutdown inmain()’sfinallyblock still run before the process exits. (The explicit catch is also there to avoid Node’s default unhandled-rejection handler, which would dump the bundled minified frame for top-level rejections, and to keep the stderr flush deterministic across the supported Node range.) -
The fix for an OAuth session is to re-run
arkor login --oauth, which goes through the full PKCE flow again and overwrites~/.arkor/credentials.jsonwith fresh tokens.
@arkor/cloud-api-client’s getToken() and is on the SDK roadmap. If an anonymous session starts failing today, run arkor login --anonymous to mint a new one (this issues a new anonymousId, so it is effectively a different workspace).