External Credential Sync NEW
Problem
Users manage AI API credentials across multiple tools—CLIs (Claude Code, Codex CLI), web portals, and local development environments. Manually re-entering credentials for each tool is friction-prone and leads to:
- Stale tokens: OAuth refresh tokens expire, causing authentication failures
- Inconsistent state: Credentials updated in one tool don't propagate to others
- Token-only drift: Some tools support OAuth refresh; others store static tokens that expire
Agents need automatic credential synchronization to reduce authentication friction while maintaining security (no plaintext storage, proper expiry handling).
Solution
Cross-credential-source synchronization with near-expiry detection, type-aware upgrades, and duplicate detection. The system reads credentials from external tool stores (keychain, config files) and syncs them into the agent's credential store, with intelligent merging and freshness tracking.
Core concepts:
- Source plugins: Each external tool (Claude CLI, Codex CLI, Qwen Portal) implements a credential reader that accesses its secure storage (keychain, encrypted config).
- Near-expiry detection: Credentials within 24 hours of expiration trigger proactive refresh, preventing auth failures during active sessions.
- Type-aware upgrades: OAuth credentials are preferred over token-only credentials. When sync detects an OAuth credential for a profile that previously had a token-only credential, it upgrades to enable auto-refresh.
- Duplicate detection: Compares credential values (access tokens, refresh tokens) to avoid creating duplicate profiles for the same underlying account.
- TTL-based caching: External reads are cached for 5 minutes to avoid excessive keychain access while maintaining freshness.
- Immutable profile IDs: Each external source maps to a fixed profile ID (e.g.,
claude-cli,codex-cli), allowing stable references across sync cycles.
Implementation sketch:
const EXTERNAL_CLI_NEAR_EXPIRY_MS = 24 * 60 * 60 * 1000; // 24 hours
const EXTERNAL_CLI_SYNC_TTL_MS = 5 * 60 * 1000; // 5 minutes cache
function isExternalProfileFresh(cred: Credential, now: number): boolean {
if (cred.type !== "oauth" && cred.type !== "token") return false;
if (!["anthropic", "openai-codex", "qwen-portal"].includes(cred.provider)) return false;
if (typeof cred.expires !== "number") return true; // No expiry = assume fresh
return cred.expires > now + EXTERNAL_CLI_NEAR_EXPIRY_MS;
}
function syncExternalCliCredentials(store: CredentialStore): boolean {
let mutated = false;
const now = Date.now();
// Sync from Claude Code CLI
const existingClaude = store.profiles["claude-cli"];
const shouldSyncClaude = !existingClaude || !isExternalProfileFresh(existingClaude, now);
if (shouldSyncClaude) {
const claudeCreds = readClaudeCliCredentialsCached({
ttlMs: EXTERNAL_CLI_SYNC_TTL_MS,
});
if (claudeCreds) {
// Upgrade token->oauth if CLI now has OAuth
const shouldUpgrade = existingClaude?.type === "token" && claudeCreds.type === "oauth";
const isMoreRecent = claudeCreds.expires > (existingClaude?.expires ?? 0);
if (shouldUpgrade || isMoreRecent) {
store.profiles["claude-cli"] = claudeCreds;
mutated = true;
}
}
}
// Repeat for other sources (Codex CLI, Qwen Portal)...
return mutated;
}
Duplicate detection for Codex CLI:
function findDuplicateCodexProfile(store: CredentialStore, creds: OAuthCredential): string | undefined {
for (const [profileId, profile] of Object.entries(store.profiles)) {
if (profileId === "codex-cli") continue;
if (profile.provider !== "openai-codex") continue;
if (profile.access === creds.access && profile.refresh === creds.refresh) {
return profileId; // Same credentials exist under different profile
}
}
return undefined;
}
Type upgrade preference:
// Prefer OAuth over token-only (enables auto-refresh)
if (existing?.type === "token" && claudeCreds.type === "oauth") {
store.profiles[CLAUDE_CLI_PROFILE_ID] = claudeCreds;
mutated = true;
}
// Never downgrade OAuth to token
if (existing?.type === "oauth" && claudeCreds.type === "token") {
// Skip update; preserve OAuth capability
}
How to use it
- Identify credential sources: Map external tools that store credentials for the same providers (Anthropic, OpenAI, etc.).
- Implement credential readers: For each source, write a function that reads credentials from its secure store (keychain, config file).
- Define profile IDs: Assign stable IDs to each external source (e.g.,
claude-cli,codex-cli). - Sync on startup and timer: Run sync when the agent starts and periodically (e.g., every hour) to refresh near-expiry credentials.
- Handle upgrade paths: When OAuth becomes available for a token-only profile, upgrade automatically.
- Detect duplicates: Before creating a new profile, check for existing profiles with the same credential values.
Pitfalls to avoid:
- Excessive keychain reads: Cache external reads to avoid triggering OS security prompts too frequently.
- Missing expiry handling: Some credentials don't carry expiry (Codex CLI). Use file mtime as a heuristic.
- OAuth downgrade risk: Never replace OAuth credentials with token-only credentials; this loses auto-refresh capability.
- Race conditions: Multiple syncs running concurrently can overwrite credentials. Use file locks or mutexes.
Trade-offs
Pros:
- Reduced friction: Users authenticate once per provider; all tools benefit.
- Proactive refresh: Near-expiry detection prevents auth failures during active sessions.
- Type upgrades: OAuth adoption is automatic when tools upgrade from token-only to OAuth.
- Duplicate elimination: Avoids cluttering credential store with redundant profiles.
Cons/Considerations:
- Keychain dependency: Requires access to OS keychain, which may fail in headless environments.
- Platform differences: Windows, macOS, and Linux keychain APIs differ; need abstractions.
- Privilege requirements: Reading keychain credentials may require user permission or elevated privileges.
- Sync lag: Cached reads (5-minute TTL) mean fresh credentials may not appear immediately.
References
- Clawdbot external-cli-sync.ts - Sync logic
- Clawdbot CLI credential readers - Keychain access
- Clawdbot credential types - Type definitions
- Related: PII Tokenization for credential security patterns