Repository navigation
Description
Client or integration
OpenCodex dashboard
Area
Authentication and account pool
Summary
When adding or reauthenticating a Codex account in the managed pool via OAuth in the dashboard or CLI, OpenCodex allows registering a pool account that has the exact same ChatGPT account ID, subscription plan bucket, and email as the native Codex main account (__main__ in ~/.codex/auth.json).
Once added, the dashboard displays this duplicate account alongside the native main account. Selecting this pool account or allowing the account pool rotation to select it subsequently dispatches inference requests using that credential, exhausting the native main account's quota under the impression that an alternate account is being utilized.
In addition, extractEmail in src/oauth/chatgpt-shared.ts only looks for top-level payload.email, omitting OpenAI's namespaced payload["https://api.openai.com/profile"]?.email. As a result, email extraction fails during OAuth token exchange for access tokens, causing pool accounts to record alias IDs rather than real account emails, which blinds email-based collision prevention.
Reproduction
- Log in to Codex CLI natively with Account A (e.g.
main@example.com, Team/Plus subscription), which populates~/.codex/auth.jsonwithaccount_id(e.g.746bcddc-...). - Start OpenCodex:
ocx start --port 10100. - Open the OpenCodex dashboard (
http://localhost:10100) and navigate to Codex Auth. - Click Add Account to initiate OAuth login.
- In the browser popup/tab, if the browser is currently signed into Account A, OpenAI completes OAuth authorization and issues tokens for Account A.
- OpenCodex imports this credential into
config.codexAccountsandcodex-accounts.jsonunder a new pool ID (e.g.account-2). - Observe: OpenCodex accepts Account A into the pool without flagging a duplicate collision with the native main account.
- Pin or select
account-2in OpenCodex and send prompts through Codex CLI. - Observe: The native main account's 5-hour and weekly quota are drained because
account-2is an identical clone of__main__.
Expected behavior:
OpenCodex should detect that the newly authorized credentials match the native Codex main account's ChatGPT account ID, plan bucket, and email, and reject registration with a clear error: Account is already configured as the native Codex main account.
Root cause analysis
-
checkAccountIdCollisionskips native main account checks (src/codex/auth-collision.ts):- Currently,
checkAccountIdCollisiononly iterates overconfig.codexAccounts:export function checkAccountIdCollision( chatgptAccountId: string, email?: string | null, plan?: unknown, excludeAccountId?: string | null, ): { collision: true; reason: string } | { collision: false } { const candidateEmail = normalizedEmail(email); const candidateWorkspace = isWorkspacePlan(plan); for (const account of loadConfig().codexAccounts ?? []) { // only checks other pool accounts ... } return { collision: false }; }
- In commit
0da08f9588, the checkmainAccountId === chatgptAccountIdwas removed to allow users to hold separate personal and business plan buckets for the same ChatGPT account ID, and to permit different team members to share a workspace account ID. - However, removing the check entirely created a blind spot: when a pool candidate matches
__main__in the same plan bucket with the same email, it is mistakenly treated as a distinct account.
- Currently,
-
Access token email extraction defect (
src/oauth/chatgpt-shared.ts):extractEmailonly parses top-levelpayload.email:export function extractEmail(idToken?: string, accessToken?: string): string | undefined { for (const token of [idToken, accessToken]) { if (!token) continue; const payload = decodeJwtPayload(token); if (!payload) continue; if (typeof payload.email === "string") return payload.email.toLowerCase(); } return undefined; }
- In OpenAI OAuth access tokens, the email claim is located in
payload["https://api.openai.com/profile"]?.email. - Because
extractEmailreturnsundefined,login-flow.tsfalls back toemail = cred.email || accountId. When WHAM usage observation is delayed or does not supply an email, the candidate's email becomes its local ID (e.g.chatgpt-1792445816119), obscuring the account identity and blinding email-based collision prevention.
Scope and distinction from legitimate multi-workspace accounts
It is important to distinguish this defect from legitimate multi-workspace and multi-member setups, which must continue to work:
- Same user across different workspaces (e.g.,
user@example.combelonging to Workspace Acfe7a0e6-..., Workspace B89e44725-..., etc.): Each workspace has a distinctchatgptAccountIdand independent quota. These must NOT collide. - Different users sharing the same team workspace (e.g.,
alice@company.comandbob@company.comsharing89e44725-...): Different emails in the same workspace. These must NOT collide. - Duplicate of native main account: Identical
chatgptAccountId, identical plan bucket (personal vs workspace), and identical email (or missing email). This MUST be blocked.
Proposed resolution
- Extend
checkAccountIdCollisioninsrc/codex/auth-collision.tsto inspect the native main account (readCodexTokens()):- Compare candidate against native main account's
chatgptAccountIdand plan bucket (isWorkspacePlan). - If both match and emails match (or either email is absent), return
{ collision: true, reason: "Account is already configured as the native Codex main account." }. - Skip checking the main account when
excludeAccountId === MAIN_CODEX_ACCOUNT_ID.
- Compare candidate against native main account's
- Update
extractEmailinsrc/oauth/chatgpt-shared.tsto inspectpayload["https://api.openai.com/profile"]?.emailin addition to top-levelpayload.email. - Add regression tests covering:
- Rejection of pool accounts matching the native main login in the same plan bucket and email.
- Allowance of different workspace members sharing the main login's
chatgptAccountId. - Exemption when
__main__is explicitly excluded (reauth). - Profile-claim email parsing from access tokens.
Pull request fixing this issue: #7399
Version
2.89.0 (commit d3dafd3)
Operating system
macOS 15.5
Checks
- I searched existing issues and documentation.
- I removed secrets, tokens, account details, request credentials, and personal data.
Activity
github-actions commentedon Oct 19, 2026
Automated translation bookkeeping — detected language: English.
fix(codex): stop pool accounts from spending the native main account (#…
Ingwannu commentedon Oct 20, 2026
Checked against trusted dev
12025981e5a304cdb6b1b54f525ba8554f623977. The reported admission gap and namespaced profile-email parsing are now addressed by merged #7414 (the current dev tip).auth-collision.tscompares the lifecycle-published native identity while preserving separate workspace/personal buckets and distinct members;chatgpt-shared.tsreads the namespaced profile email. The integration also excludes pre-existing native clones rather than only blocking new admissions. Closing as fixed on dev, not as released or live-account certified. The independent pool-pin/fallback policy question remains #7410.