Documentation Index
Fetch the complete documentation index at: https://mintlify.com/ndycode/codex-multi-auth/llms.txt
Use this file to discover all available pages before exploring further.
Codex CLI Integration
Codex Multi-Auth integrates with the official @openai/codex CLI to share authentication state and account management across both tools.
How Sync Works
Storage Files
The Codex CLI stores authentication in two files:
~/.codex/
├── accounts.json # Multi-account storage (preferred)
└── auth.json # Legacy single-account auth
Sync Direction
Bidirectional sync between Codex Multi-Auth and Codex CLI:
Codex CLI → Multi-Auth (on plugin load):
- Read accounts from
~/.codex/accounts.json or ~/.codex/auth.json
- Merge accounts into local storage by account ID, refresh token, or email
- Update active account selection if Codex CLI state is newer
Multi-Auth → Codex CLI (on account switch):
- Write active account tokens to
~/.codex/auth.json
- Update
active: true flag in ~/.codex/accounts.json
- Set
activeAccountId and activeEmail metadata
Enabling Sync
Environment Variable
Sync is enabled by default. To explicitly control:
# Enable sync (default)
export CODEX_MULTI_AUTH_SYNC_CODEX_CLI=1
# Disable sync
export CODEX_MULTI_AUTH_SYNC_CODEX_CLI=0
Legacy variable (deprecated but still supported):
export CODEX_AUTH_SYNC_CODEX_CLI=1
Custom Storage Paths
Override default paths:
export CODEX_CLI_ACCOUNTS_PATH="/custom/path/accounts.json"
export CODEX_CLI_AUTH_PATH="/custom/path/auth.json"
Account Reconciliation
Deduplication Logic
When syncing from Codex CLI, accounts are deduplicated using:
- Account ID (highest priority)
- Refresh Token
- Email (case-insensitive, normalized)
Example:
// Codex CLI has:
// - Account A: id=org-123, email=user@example.com, refresh=token-abc
// Multi-Auth has:
// - Account B: id=org-123, email=old@example.com, refresh=token-xyz
// After sync, Account B is updated:
// - Account B: id=org-123, email=user@example.com, refresh=token-abc
Active Selection Priority
Active account is determined by timestamp comparison:
const codexVersion = state.syncVersion || state.sourceUpdatedAtMs;
const localVersion = max(
getLastAccountsSaveTimestamp(),
getLastCodexCliSelectionWriteTimestamp()
);
if (codexVersion >= localVersion - toleranceMs) {
// Apply Codex CLI selection
} else {
// Keep local Multi-Auth selection
}
Tolerance is 0ms for syncVersion (explicit), 1000ms for file mtime (best-effort).
accounts.json
{
"accounts": [
{
"accountId": "org-abc123",
"email": "user@example.com",
"auth": {
"tokens": {
"access_token": "ey...",
"refresh_token": "ey...",
"id_token": "ey...",
"expires_at": 1709500000000,
"account_id": "org-abc123"
}
},
"active": true,
"isActive": true,
"is_active": true
}
],
"activeAccountId": "org-abc123",
"active_account_id": "org-abc123",
"activeEmail": "user@example.com",
"active_email": "user@example.com",
"codexMultiAuthSyncVersion": 1709500000000
}
auth.json
{
"auth_mode": "chatgpt",
"email": "user@example.com",
"tokens": {
"access_token": "ey...",
"refresh_token": "ey...",
"id_token": "ey...",
"account_id": "org-abc123"
},
"last_refresh": "2024-03-03T12:00:00.000Z",
"OPENAI_API_KEY": null,
"codexMultiAuthSyncVersion": 1709500000000
}
Sync Version
The codexMultiAuthSyncVersion field is a Unix timestamp (milliseconds) written by Codex Multi-Auth to track the last sync operation. This enables deterministic active-account selection when both tools are used concurrently.
Writer Module
setCodexCliActiveSelection
Writes active account to Codex CLI state:
import { setCodexCliActiveSelection } from "codex-multi-auth/lib/codex-cli/writer";
await setCodexCliActiveSelection({
accountId: "org-abc123",
email: "user@example.com",
accessToken: "ey...",
refreshToken: "ey...",
expiresAt: 1709500000000,
idToken: "ey..."
});
Returns: true if write succeeded, false if sync is disabled or files missing.
Write Queue
Writes are serialized through a queue to prevent race conditions:
// Multiple concurrent writes are queued
await Promise.all([
setCodexCliActiveSelection(account1),
setCodexCliActiveSelection(account2),
setCodexCliActiveSelection(account3)
]);
// Executes sequentially: account1 → account2 → account3
Atomic Write Strategy
Writes use temp files + rename for atomic updates:
// 1. Write to temp file
await fs.writeFile(
`${path}.${Date.now()}.tmp`,
JSON.stringify(payload, null, 2),
{ mode: 0o600 } // Read/write for owner only
);
// 2. Atomic rename (retries on EBUSY/EPERM)
for (let attempt = 0; attempt < 5; attempt++) {
try {
await fs.rename(tempPath, path);
break;
} catch (error) {
if (error.code === "EBUSY" || error.code === "EPERM") {
await sleep(10 * 2 ** attempt); // Exponential backoff
} else {
throw error;
}
}
}
// 3. Clean up temp file
await fs.unlink(tempPath).catch(() => {});
Sync Module
syncAccountStorageFromCodexCli
Reconciles local storage with Codex CLI state:
import { syncAccountStorageFromCodexCli } from "codex-multi-auth/lib/codex-cli/sync";
const current = await loadAccounts(); // Local storage
const { storage, changed } = await syncAccountStorageFromCodexCli(current);
if (changed) {
await saveAccounts(storage);
}
Returns:
storage: Reconciled storage (may be same as current if no changes)
changed: true if storage was modified
Active Index Normalization
The sync module ensures all active indexes are valid:
function normalizeStoredFamilyIndexes(storage: AccountStorageV3): void {
const count = storage.accounts.length;
// Clamp global activeIndex to [0, count-1]
storage.activeIndex = Math.max(0, Math.min(storage.activeIndex, count - 1));
// Clamp per-family indexes
for (const family of ["codex", "gpt-5.1", "o1", "o3"]) {
const raw = storage.activeIndexByFamily[family] ?? storage.activeIndex;
storage.activeIndexByFamily[family] = Math.max(0, Math.min(raw, count - 1));
}
}
State Module
loadCodexCliState
Reads Codex CLI state with caching:
import { loadCodexCliState } from "codex-multi-auth/lib/codex-cli/state";
const state = await loadCodexCliState();
if (state) {
console.log(`Loaded ${state.accounts.length} accounts`);
console.log(`Active: ${state.activeAccountId} (${state.activeEmail})`);
console.log(`Sync version: ${state.syncVersion}`);
}
Returns: CodexCliState | null
Cache: 5-second TTL in-memory cache (bypass with forceRefresh: true)
lookupCodexCliTokensByEmail
Retrieve tokens for a specific email:
import { lookupCodexCliTokensByEmail } from "codex-multi-auth/lib/codex-cli/state";
const tokens = await lookupCodexCliTokensByEmail("user@example.com");
if (tokens) {
console.log(`Access token: ${tokens.accessToken}`);
console.log(`Refresh token: ${tokens.refreshToken}`);
console.log(`Expires at: ${new Date(tokens.expiresAt)}`);
}
Returns: CodexCliTokenCacheEntry | null
Observability
The sync modules track metrics:
import { getCodexCliMetrics } from "codex-multi-auth/lib/codex-cli/observability";
const metrics = getCodexCliMetrics();
console.log(JSON.stringify(metrics, null, 2));
Metric keys:
readAttempts - Calls to loadCodexCliState
readSuccesses - Successful state loads
readFailures - Failed reads (malformed JSON, ENOENT)
readMisses - No Codex CLI files found
writeAttempts - Calls to setCodexCliActiveSelection
writeSuccesses - Successful writes
writeFailures - Failed writes
reconcileAttempts - Calls to syncAccountStorageFromCodexCli
reconcileChanges - Reconciliations that modified storage
reconcileNoops - Reconciliations with no changes
reconcileFailures - Failed reconciliations
legacySyncEnvUses - Uses of deprecated CODEX_AUTH_SYNC_CODEX_CLI
Concurrency Considerations
File Locking
The sync implementation does not use file locks. Instead:
- Atomic writes via temp file + rename
- Retry logic for EBUSY/EPERM errors (Windows antivirus)
- Sync version timestamps for last-write-wins resolution
This works well for typical use cases but may race if both tools write simultaneously.
Live Account Sync
When CODEX_LIVE_ACCOUNT_SYNC=1, the plugin watches Codex CLI files:
import { LiveAccountSync } from "codex-multi-auth/lib/live-account-sync";
const liveSync = new LiveAccountSync(
async () => {
// Reload account manager from disk
await reloadAccountManagerFromDisk();
},
{
debounceMs: 500, // Wait 500ms after file change
pollIntervalMs: 5000 // Poll every 5s as fallback
}
);
await liveSync.syncToPath(
process.env.CODEX_CLI_ACCOUNTS_PATH || "~/.codex/accounts.json"
);
Triggers reload when Codex CLI modifies accounts.json or auth.json.
Migration Scenarios
From Codex CLI to Multi-Auth
- Existing Codex CLI accounts are auto-imported on first Multi-Auth use
- Active selection is preserved based on file timestamps
- No manual migration needed
From Multi-Auth to Codex CLI
- Switch account in Multi-Auth dashboard
- Active selection syncs to
~/.codex/auth.json
- Run
codex auth status to verify
- Login via Codex CLI:
codex auth login
- Account appears in Multi-Auth dashboard automatically
- Switch account in either tool
- Selection syncs within 5 seconds (or immediately if live sync enabled)
Troubleshooting
Sync Not Working
Check sync status:
# Verify sync is enabled
codex-multi-auth auth status
# Check Codex CLI files exist
ls -la ~/.codex/accounts.json ~/.codex/auth.json
# Enable debug logging
export DEBUG="codex-multi-auth:codex-cli-*"
codex-multi-auth auth list
Conflicting Active Accounts
If active account differs between tools:
# Force sync from Multi-Auth to Codex CLI
codex-multi-auth auth switch 0 # Select first account
# Verify sync
codex auth status
Permission Errors (Windows)
If writes fail with EBUSY/EPERM:
- Close any editors with
~/.codex/*.json open
- Temporarily disable antivirus file monitoring for
~/.codex/
- Retry operation
The plugin retries 5 times with exponential backoff, so transient locks usually resolve.
Next Steps