Accounts, auth, and regions
The Auth0 PKCE handoff through Redis, the HB Code JWT and its refresh, what a region change invalidates, how a phone reads and switches Codex profiles and Claude accounts, and which data leaves your computer.
Checked against the source on 5 October 2026
On this page
How the native sign-in returns to the app
- State keyauth0:state:{state}
Written by initiate-login with the polling ID and CSRF token, and deleted by the callback as it parks the code. A replayed callback finds nothing and gets 401.
- Poll keyauth0:poll:{pid} · SET NX
Polls read it without consuming it, so after a lost reply the next poll gets the same code. finalize-login deletes it after issuing the JWT, and both keys expire after 30 minutes.
- PollingGET /auth0/poll-status
Phones poll every 2 seconds, up to 60 times. The desktop polls every 2 seconds for about a minute, then every 3, and stops after 60 polls or 150 seconds.
- Verifiercode_verifier
Created with the polling ID and kept in app memory. Only Auth0’s token endpoint receives it, once, so a parked or intercepted code is useless without it.
- Access tokenfinalize-login · auth0_id_token
The app gets it from its own code exchange and posts it once to finalize-login, in a field still named auth0_id_token. The server validates it, fetches userinfo with it, and stores it nowhere.
- Refresh tokenusers.auth0_refresh_token
Passes through the app once. The server stores it encrypted, exchanges it with Auth0 using its own client credentials, and keeps the rotated token even when the new tokens then fail validation.
- HB Code JWTJWT_ACCESS_TOKEN_DURATION_DAYS = 7
Issued per device with a device_id claim. Up to 24 hours after it expires, refresh-app-token still trades it for a new one. After that you sign in again.
- ChatGPT sign-incodex --auth-home
Created by the Codex child process’s own login and kept in the auth home of the desktop profile. Phones get the profile’s name, email, plan, and usage limits over the relay, never the sign-in itself.
- Claude sign-inclaude auth login --claudeai
Created by Claude’s own sign-in command, which the desktop runs for one account folder. The desktop never reads the token. Phones get the account’s email, organization, plan, and usage windows over the relay.
| HTTP entry point | Purpose |
|---|---|
| GET / | Begin account login. |
| GET / | Auth0 redirects the browser here. Park the code and send the browser to a confirmation page. |
| GET / | Non-consuming poll; 204 with Cache-Control: no-store until the code is parked. |
| POST / | Validate the Auth0 token and client ID, require a verified email, issue the HB Code JWT, grant first-login credits, then delete the poll key when the request carries its polling ID and CSRF token. |
| POST / | Refresh the HB Code app token. The only route that accepts an expired token. |
Consuming the state key and deleting the poll key each run as one Lua script in Redis. The store is Redis rather than memory because every API process in the region shares it.
The HB Code JWT is HS256 with a shared secret, issuer hbcode, audience hbcode-api, and scope read write rpc. Its jti is checked against a revocation table on every request and again at the WebSocket upgrade, its device_id claim is compared to the X-Device-ID header, and it lives 7 days by default. The refresh route accepts a token for up to 24 hours after expiry, with issuer, audience, device binding, scopes, and revocation still enforced, runs in a per-user serialized transaction, and refuses to mint a token if the Auth0 subject or email no longer matches the stored user. The WebSocket upgrade captures a per-user registration fence before its revocation check, so a logout that completes between the HTTP check and relay registration invalidates the registration.
The server’s optional server-client credential pair is needed for server-side Auth0 refresh exchange. Configure the native app’s Auth0 domain, native client ID, audience, and callback settings together. The selected API region and the iOS authServerURL are distinct configuration paths, so verify both when creating a local environment.
What a region change or sign-out invalidates
Changing the region or signing out ends the previous attempt and clears its credentials. A late result from an old attempt cannot restore an account, because every result is bound to the attempt and the origin that started it.
Desktop login, logout, token refresh, and session reconciliation share one mutation lock, and reconciliation discards results when the token or origin changed during a request. Callback replay is rejected, and handoff acknowledgement is idempotent.
Agent accounts from a phone
Codex profiles and Claude accounts live on the desktop. A phone reads them and picks the active one through six relay methods in the system namespace. Four methods read. Two methods switch, and both are mutations that need an idempotencyKey.
| Method | Params and result |
|---|---|
| system. | Read, no params. Returns profiles. Each entry has profile |
| system. | Mutation, param profile |
| system. | Read, no params. Returns the limits of the active Codex profile: primary and secondary, each with used |
| system. | Read, no params. Returns accounts. Each entry has account |
| system. | Mutation, param account |
| system. | Read, param agent with the value codex or claude. Returns agent, recording |
The two switch methods accept only their one ID. An unknown field, a snake_case field, or a blank ID is an invalid-params error. system.agentUsageHistory rejects every agent value other than codex and claude.
- Switch callsystem.activateClaudeAccount · system.activateCodexAuthProfile
Params are one ID, accountId or profileId. Both methods are mutations. The relay refuses them without an idempotencyKey with error -32026.
- Settingagent.chat.claude_account_id · agent.chat.codex_profile_id
The switch is one settings write on the desktop. It stops no process. The desktop then emits agent-accounts-changed to its own window.
- Stored answeridempotencyKey
The desktop keeps the finished answer under the key. A retry with the same key and params gets it back, so a lost reply cannot cause a second switch.
- Next turnCLAUDE_CONFIG_DIR
A Claude turn reads the active account once, when it starts, and spawns its host with the folder of that account. A Codex switch also stops no child process. A running Codex turn keeps its profile, and the next Codex turn signs in with the active profile.
A switch is one settings write. It stops no process, and it changes only which sign-in the next turn uses. Session history does not move.
| Step | Codex | Claude |
|---|---|---|
| Check | Takes the profile lifecycle lock and checks that the profile exists. | Finds the account folder. An unknown account is an error. |
| Write | Sets agent. | Sets agent. |
| Running turn | Keeps the profile pair it started with. A steer message goes to that same Codex child process. | Keeps its host and its account folder until the turn ends. |
| Next turn of an existing session | Signs in with the profile that is active when the turn is admitted. The session history stays in the profile that stores it. A turn is refused when the active profile is not signed in. | Reads the setting once, when it starts, and runs on the active account. Every account reads and continues the same sessions. |
After the write the desktop emits the event agent-accounts-changed with the agent name to its own window. The sidebar and the settings page read again. A failed emit is only logged, because the switch is already stored and the desktop window catches up at its next periodic read.
The relay refuses a switch without a key with error -32026. The desktop stores the finished answer under the key. A retry with the same key and the same params gets that stored answer, and the same key with other params is a conflict.
| Read | Freshness | Limit |
|---|---|---|
| Claude accounts | One reading per account for 60 seconds. A reading runs claude auth status --json with a 15 second timeout and, for a signed-in account, one usage read. | A failed read becomes error |
| Codex usage limits | One cached value for 60 seconds, read through account/ | The cache has no profile key and a switch does not clear it. For up to 60 seconds after a switch the phone can see the limits of the previous profile. |
| Usage history, weekly samples | The desktop saves each fresh limit reading in the table agent_ | A reading that repeats the newest one within 10 minutes, with the same reset time and less than 0.5 percentage points of change, is skipped. Rows are kept for 35 days. The answer holds the last 14 days of the active account, oldest first. |
| Usage history, tokens | Codex only. daily | Both fields are null for Claude and for a Codex sign-in without history, such as an API key. |
Local execution still uses remote services
Tools and file edits run on the desktop, and a model request still sends the task context to the provider of the session’s agent: OpenAI for Codex, Anthropic for Claude. Mobile control sends requests and the returned workspace content through the regional server. Dictation sends a recording to the transcription service you configured, and the agent’s Gemini tools send text or video to Gemini under your key.
So “desktop-owned” describes where execution and files live. It is neither an offline mode nor an end-to-end encryption claim.