Back to Codexbar

Keychain Fix: Current State

docs/KEYCHAIN_FIX.md

0.246.4 KB
Original Source

Keychain Fix: Current State

Scope change from the original doc

The original fix (migrating legacy CodexBar keychain items to kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly) is still in place, but the architecture has changed:

  • Provider settings and manual secrets are now persisted in ~/.codexbar/config.json.
  • Legacy keychain stores are still present mainly to migrate old installs, then clear old items.
  • Keychain is still used for runtime cache entries (for example com.steipete.codexbar.cache) and Claude OAuth bootstrap reads from Claude CLI keychain (Claude Code-credentials).

Then vs now

Previous statement in this docCurrent behavior
CodexBar stores provider credentials only in keychainManual/provider settings are config-file backed (~/.codexbar/config.json), while keychain is still used for runtime caches and Claude OAuth bootstrap fallback.
ClaudeOAuthCredentials.swift migrated CodexBar-owned Claude OAuth keychain itemsClaude OAuth primary source is Claude CLI keychain service (Claude Code-credentials), with CodexBar cache in com.steipete.codexbar.cache (oauth.claude).
Migration runs in CodexBarApp.init()Migration runs in HiddenWindowView .task via detached task (KeychainMigration.migrateIfNeeded()).
Post-migration prompts should be zero in all Claude pathsLegacy-store prompts are reduced; Claude OAuth bootstrap can still prompt when reading Claude CLI keychain, with cooldown + no-UI probes to prevent storms.
Log category is KeychainMigrationCategory is keychain-migration (kebab-case).

Current keychain surfaces for Claude

1. Legacy CodexBar keychain migration (V1)

Sources/CodexBar/KeychainMigration.swift migrates legacy com.steipete.CodexBar items (for example claude-cookie) to AfterFirstUnlockThisDeviceOnly.

  • Gate key: KeychainMigrationV1Completed
  • Runs once unless flag is reset.
  • Covers legacy CodexBar-managed accounts only (not Claude CLI's own keychain service).

2. Claude OAuth bootstrap path

Sources/CodexBarCore/Providers/Claude/ClaudeOAuth/ClaudeOAuthCredentials.swift

Load order for credentials:

  1. Environment override (CODEXBAR_CLAUDE_OAUTH_TOKEN, scopes env key).
  2. In-memory cache.
  3. CodexBar keychain cache (com.steipete.codexbar.cache, account oauth.claude).
  4. ~/.claude/.credentials.json.
  5. Claude CLI keychain service: Claude Code-credentials (promptable fallback).

Prompt mitigation:

  • Non-interactive keychain probes use KeychainNoUIQuery with LAContext.interactionNotAllowed.
  • Pre-alert is shown only when preflight suggests interaction may be required.
  • Denials are cooled down in the background via claudeOAuthKeychainDeniedUntil (ClaudeOAuthKeychainAccessGate). User actions (menu open / manual refresh) clear this cooldown.
  • Auto-mode availability checks use non-interactive loads with prompt cooldown respected.
  • Background cache-sync-on-change also performs non-interactive Claude keychain probes (syncWithClaudeKeychainIfChanged) and can update cached OAuth data when the token changes.

Why two Claude keychain prompts can still happen on startup

When CodexBar does not have usable OAuth credentials in its own cache (com.steipete.codexbar.cache / oauth.claude), bootstrap falls through to Claude CLI keychain reads.

Current flow can perform up to two interactive reads in one bootstrap call:

  1. Interactive read of the newest discovered keychain candidate.
  2. If that does not return usable data, interactive legacy service-level fallback read.

On some macOS keychain/ACL states, pressing Allow (session-only) for the first read does not grant enough access for the second read shape, so macOS prompts again. Pressing Always Allow usually authorizes both query shapes for the app identity and avoids the immediate second prompt.

The prompt copy differs because Security.framework is authorizing different operations:

  • one path is a direct secret-data read for the key item,
  • the fallback path is a key/service access query.

This is OS/keychain ACL behavior, not a ThisDeviceOnly migration issue.

Sources/CodexBarCore/CookieHeaderCache.swift and Sources/CodexBarCore/KeychainCacheStore.swift

  • Browser-imported Claude session cookies are cached in keychain service com.steipete.codexbar.cache.
  • Account key is cookie.claude.
  • Cache writes use kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly.
  • Users can clear browser-cookie cache entries from Preferences → Debug → Caches or with codexbar cache clear --cookies. --provider <id> scopes cookie clearing to one provider and includes scoped Codex managed-account cookie keys.

What still uses ThisDeviceOnly

  • Legacy store implementations (CookieHeaderStore, token stores, MiniMax stores) still write using kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly.
  • Keychain cache store (com.steipete.codexbar.cache) also writes with ThisDeviceOnly.

Disable keychain access behavior

Advanced -> Disable Keychain access sets debugDisableKeychainAccess and flips KeychainAccessGate.isDisabled.

Effects:

  • Blocks keychain reads/writes in legacy stores.
  • Disables keychain-backed cookie auto-import paths.
  • Forces cookie source resolution to manual/off where applicable.

Verification

Check legacy migration flag

bash
defaults read com.steipete.codexbar KeychainMigrationV1Completed

Check Claude OAuth keychain cooldown

bash
defaults read com.steipete.codexbar claudeOAuthKeychainDeniedUntil
bash
log show --predicate 'subsystem == "com.steipete.codexbar" && (category == "keychain-migration" || category == "keychain-preflight" || category == "keychain-prompt" || category == "keychain-cache" || category == "claude-usage" || category == "cookie-cache")' --last 10m

Reset migration for local testing

bash
defaults delete com.steipete.codexbar KeychainMigrationV1Completed
./Scripts/compile_and_run.sh

Key files (current)

  • Sources/CodexBar/KeychainMigration.swift
  • Sources/CodexBar/HiddenWindowView.swift
  • Sources/CodexBarCore/Providers/Claude/ClaudeOAuth/ClaudeOAuthCredentials.swift
  • Sources/CodexBarCore/Providers/Claude/ClaudeOAuth/ClaudeOAuthKeychainAccessGate.swift
  • Sources/CodexBarCore/KeychainAccessPreflight.swift
  • Sources/CodexBarCore/KeychainNoUIQuery.swift
  • Sources/CodexBarCore/KeychainCacheStore.swift
  • Sources/CodexBarCore/CookieHeaderCache.swift