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_KEYin the server environment, and considerrequireApiKey: trueoncreateWorkerServerto 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 -pyourself) is the one case where subscription credentials may be appropriate. WithoutrequireApiKey, the server allows it but logs a one-time notice; the auth provenance is also visible per session asapiKeySourceonSessionInfoand thesystem_initevent.
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
0600file in the profile’s config dir, injected into that profile’s sessions asCLAUDE_CODE_OAUTH_TOKEN. It never appears in an API response, an event,session_infoor a log; clients see onlyconnected,connectedAtandexpiresAt. A connected profile’s sessions dropANTHROPIC_API_KEYandANTHROPIC_AUTH_TOKENfrom the gateway environment, which would otherwise outrank the token. requireApiKeywins. A session on a connected token reportsapiKeySource: 'none', so withrequireApiKey: truethe 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: falseon a profile turns claude.ai connectors off explicitly for any login (ENABLE_CLAUDEAI_MCP_SERVERS=false). - Who may connect.
canManageProfilesprincipals by default; an embedder can widen that per profile withaccounts.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-tokenandcodex 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.
Related
- Deployment - the host-app auth hook
(
authenticate), which is a separate concern from Anthropic credentials. - Server reference -
requireApiKeyand the rest of the options.