WorkerDeck

Guides

Auth & the providers' terms

WorkerDeck performs no model-provider authentication of its own - what that means for operators and contributors.

WorkerDeck performs no Anthropic authentication of its own - by design. It spawns the official Agent SDK, which spawns the official Claude Code CLI, which resolves whatever credentials the operator’s environment provides: ANTHROPIC_API_KEY, Bedrock/Vertex platform auth, the operator’s own stored claude login, or a profile’s connected account. WorkerDeck never implements claude.ai OAuth itself and never reads ~/.claude credentials. Which credentials your deployment uses - and whether that use complies with Anthropic’s terms - is the operator’s responsibility.

Where we understand the lines to be

Not legal advice:

  • API key (or Bedrock/Vertex) is the supported path for anything that is a service: unattended/scheduled runs, multi-user deployments, anything you expose to others. Anthropic’s Agent SDK docs are explicit that third-party developers may not offer claude.ai login or subscription rate limits in their products; the Consumer Terms restrict automated access except via API key. Set ANTHROPIC_API_KEY in the server environment, and consider requireApiKey: true on createWorkerServer to fail closed: sessions that initialize on subscription credentials (apiKeySource: 'oauth') are terminated with an error.
  • Your own subscription, your own single-user use (the equivalent of running claude -p yourself) is the one case where subscription credentials may be appropriate. Without requireApiKey, the server allows it but logs a one-time notice; the auth provenance is also visible per session as apiKeySource on SessionInfo and the system_init event.

requireApiKey: fail closed

const worker = createWorkerServer({
  authenticate,
  requireApiKey: true, // recommended for services and any unattended use
})

Each session’s credential provenance surfaces as apiKeySource on SessionInfo and the system_init event; 'oauth' means claude.ai subscription credentials, other values ('user' | 'project' | 'org' | 'temporary') are API-key provenance. With requireApiKey: true, an 'oauth' session is terminated with a session_error telling the operator to set ANTHROPIC_API_KEY (or Bedrock/Vertex auth). Without it, the server logs a one-time notice instead - appropriate only for personal single-user deployments.

Profiles on shared machines

Profiles let one worker serve several operators, each under their own Claude Code config dir - selected via the CLI’s own CLAUDE_CONFIG_DIR mechanism, never by touching the credential chain. The auth-relevant part: scope profiles per caller with allowedProfiles on the authenticate principal. A shared dashboard where anyone may run under anyone’s account is multi-account pooling - exactly the red line below - while each person running under their own profile is just each person using their own account. The subscription notice logs per profile, and apiKeySource shows what each session actually used.

Connect a Claude account to a profile

A managed Claude profile can carry its own long-lived token, so a person can sign their own profile in from the dashboard, VS Code or an embedding app without a terminal on the gateway host. WorkerDeck does not do the OAuth: it runs the official claude setup-token under a pseudo-terminal, shows you the sign-in link that command prints, passes back the code you paste, and reads the token the command prints when it succeeds.

  • Custody. The token is valid for a year and is stored only for that profile: a 0600 file in the profile’s config dir, injected into that profile’s sessions as CLAUDE_CODE_OAUTH_TOKEN. It never appears in an API response, an event, session_info or a log; clients see only connected, connectedAt and expiresAt. A connected profile’s sessions drop ANTHROPIC_API_KEY and ANTHROPIC_AUTH_TOKEN from the gateway environment, which would otherwise outrank the token.
  • requireApiKey wins. A session on a connected token reports apiKeySource: 'none', so with requireApiKey: true the gateway refuses to connect an account and never injects a stored one.
  • Disconnect deletes the file. It does not revoke the token; revoke it at claude.ai.
  • Expiry. 30 days before the year is up the profile card shows “expires soon” and the gateway logs a line; once it has passed, the profile reports unavailable with a “reconnect” reason.
  • Scope. The token can only make model requests: no claude.ai connectors, no Remote Control. Configure MCP servers on the gateway or the profile instead. connectors: false on a profile turns claude.ai connectors off explicitly for any login (ENABLE_CLAUDEAI_MCP_SERVERS=false).
  • Who may connect. canManageProfiles principals by default; an embedder can widen that per profile with accounts.canConnect(principal, profile) (for example “the profile’s own user”). Declared profiles cannot be connected: their credentials are configuration.
  • Terms. Anthropic documents the token for “CI pipelines, scripts, or other environments where interactive browser login isn’t available” (authentication), and the Agent SDK overview says third-party developers may not offer claude.ai login unless approved. Our reading: a self-hosted harness on one person’s own subscription is in scope, the same case as running the CLI yourself. That is our reading, not legal advice; the call is the operator’s. One token per person, never shared.

The routes are POST /profiles/:name/account/connect (answers { attemptId, authorizeUrl }), POST /profiles/:name/account/complete with { attemptId, code }, and DELETE /profiles/:name/account; @workerdeck/client wraps them and @workerdeck/ui ships the dialog. One attempt per profile at a time; an attempt expires after ten minutes.

Sign a codex profile in to ChatGPT

A managed codex profile with its own codexHome can be signed in the same way. WorkerDeck runs the official codex login --device-auth with that CODEX_HOME, shows you the link and the one-time code it prints, and waits for codex to exit. codex does the sign-in and keeps the login in its own store; WorkerDeck never reads auth.json, never holds a token, and learns the outcome only from the exit code and codex login status. Sign out runs codex logout.

The same routes carry it: connect answers { attemptId, authorizeUrl, userCode }, and complete with { attemptId } alone waits up to 25 seconds, answering { pending: true } until the sign-in finishes (client.awaitAccount in a loop). An attempt lives 15 minutes, as long as the code. requireApiKey refuses it too. Whether OpenAI’s terms allow headless use of a ChatGPT subscription is unresolved; the posture is the Claude one: one person’s own account on their own profile, never shared.

Gateway auth is a separate thing entirely

Everything above is about Anthropic credentials. Guarding the gateway itself - deciding who may reach it at all - is your own concern, and the two never mix.

For an embedded deployment that is the authenticate hook: it gets the raw request and returns a principal or nothing, and it guards REST and the WebSocket upgrade. For the turnkey workerdeck instance it is --auth-key, one shared secret over two transports - a login page trades it for an HttpOnly cookie for browsers, while services send it as a header. Bound off loopback with no key supplied, the instance generates one and stores it under its state dir rather than serving open (browser logins persist there too, keyed by an HMAC under that secret, so a restart doesn’t sign every tab out and rotating the secret invalidates them all); the explicit opt-outs (--insecure, insecureHosts) are covered in Run an instance.

That split isn’t arbitrary. A browser cannot set a header on a WebSocket handshake; the constructor takes a URL and subprotocols and nothing else. So a browser-facing deployment has three options and only three: a cookie (sent automatically on a same-origin upgrade), a query-string ticket (ClientOptions.buildWsUrl exists for this, but something has to issue the ticket), or a proxy that stamps the credential server-side on the tab’s behalf. Embedding a key in the served JavaScript is not one of them.

If you take the cookie route, remember that WebSocket upgrades are exempt from CORS - an explicit Origin check, not SameSite alone, is what actually defends an attach against a cross-site page. And note that none of this establishes identity: a shared secret is a door key. Put an identity-aware proxy in front if you need to know who is on the other end.

Compliance status: settled

The compliance and legal review of this project is settled. Which credentials your deployment uses, and whether that use fits your provider’s terms, stays the operator’s responsibility.

Red lines for contributors

PRs crossing these will be rejected:

  • no hand-rolled OAuth or PKCE against any provider (the two brokers run the official claude setup-token and codex login --device-auth),
  • no refresh-token custody, no hand-written CLI credential files, no reading a CLI’s credential store,
  • no spoofing of Claude Code’s client identity,
  • no multi-account pooling, token sharing across people, or rate-limit circumvention of any kind.

The OAuth stays 100% Anthropic-owned code. Policy enforcement lives in configuration (requireApiKey, the one-time notice, apiKeySource visibility), never in tampering with the credential chain.

  • Deployment - the host-app auth hook (authenticate), which is a separate concern from Anthropic credentials.
  • Server reference - requireApiKey and the rest of the options.