Skip to content

Claude OAuth login/settings can leave Claude selected but disabled in config #816

@pdurlej

Description

@pdurlej

Summary

Claude can end up in a confusing, internally inconsistent state where the app has selectedMenuProvider = claude, but ~/.codexbar/config.json still has the Claude provider disabled. In that state Claude OAuth usage works when called directly, but the app/widget only treats Codex as enabled, so the Claude subscription/usage is not shown as expected.

This is adjacent to the existing Claude timeout/OAuth issues, but the actionable part here is the settings/login state repair: a successful Claude OAuth login or settings interaction should not leave Claude selected while disabled, and disabling a selected provider should clear the stale menu selection.

Related issues I found before filing: #212, #238, #714, #726.

Environment

  • CodexBar: 0.23 (58)
  • Build: 0a830cec, 2026-04-26T03:42:59Z
  • macOS: 26.4.1 (25E253)
  • Claude Code CLI: 2.1.123
  • Claude auth method: first-party Claude Code OAuth/keychain credentials

Observed state

The app defaults had Claude selected:

$ defaults read com.steipete.codexbar selectedMenuProvider
claude

But the config still had Claude disabled:

{
  "providers": [
    {
      "cookieSource": "off",
      "enabled": true,
      "id": "codex"
    },
    {
      "enabled": false,
      "id": "claude"
    }
  ],
  "version": 1
}

The widget snapshot consequently only listed Codex:

{
  "enabledProviders": ["codex"],
  "providers": ["codex"]
}

However, Claude OAuth itself was healthy. Calling the bundled CLI directly succeeded:

$ /Applications/CodexBar.app/Contents/Helpers/CodexBarCLI usage \
    --provider claude \
    --source oauth \
    --format json \
    --pretty \
    --no-color \
    --log-level warning

Returned usage like:

{
  "provider": "claude",
  "source": "oauth",
  "usage": {
    "primary": { "usedPercent": 27, "windowMinutes": 300 },
    "secondary": { "usedPercent": 24, "windowMinutes": 10080 }
  },
  "version": "2.1.123"
}

Manually repairing the config fixed the app:

{
  "enabled": true,
  "id": "claude",
  "source": "oauth"
}

After restart, CodexBarCLI usage --status showed both providers, including Claude OAuth:

== Claude 2.1.123 (oauth) ==
Session: 73% left
Weekly: 76% left
Status: Operational

Why this looks like an app-side bug

There are two state-safety gaps in the current code path:

  1. SettingsStore.setProviderEnabled(...) writes enabled, but if the disabled provider is also the persisted selectedMenuProvider, that stale menu selection is left behind.

  2. ClaudeProviderImplementation.runLoginFlow(...) currently returns true after runClaudeLoginFlow() regardless of whether login succeeded. runClaudeLoginFlow() posts the login notification on success, but it does not enable the Claude provider or persist source = .oauth after a successful Claude OAuth login.

The result is that a user can complete/repair Claude OAuth credentials and still have the provider disabled from the app's provider list. From the UI this looks like “Claude works in the CLI, but CodexBar does not show my Claude subscription.”

Expected behavior

  • If a provider is disabled and it is the persisted selected menu provider, clear or repair selectedMenuProvider.
  • After successful Claude login, enable the Claude provider and persist source = oauth, because that is the source the login flow just repaired.
  • Only refresh after Claude login if the login actually succeeded.
  • Optionally show a repair prompt if selectedMenuProvider points to a disabled provider on launch/config reload.

Proposed patch

This is the minimal fix direction I tested locally at the source level. It has three parts:

  • clear stale selectedMenuProvider when disabling that provider,
  • make Claude login return success/failure,
  • on successful Claude login, enable Claude and set source = .oauth before refreshing.
diff --git a/Sources/CodexBar/Providers/Claude/ClaudeLoginFlow.swift b/Sources/CodexBar/Providers/Claude/ClaudeLoginFlow.swift
index 9550abb..7600252 100644
--- a/Sources/CodexBar/Providers/Claude/ClaudeLoginFlow.swift
+++ b/Sources/CodexBar/Providers/Claude/ClaudeLoginFlow.swift
@@ -2,7 +2,7 @@ import CodexBarCore
 
 @MainActor
 extension StatusItemController {
-    func runClaudeLoginFlow() async {
+    func runClaudeLoginFlow() async -> Bool {
         let phaseHandler: @Sendable (ClaudeLoginRunner.Phase) -> Void = { [weak self] phase in
             Task { @MainActor in
                 switch phase {
@@ -12,14 +12,19 @@ extension StatusItemController {
             }
         }
         let result = await ClaudeLoginRunner.run(timeout: 120, onPhaseChange: phaseHandler)
-        guard !Task.isCancelled else { return }
+        guard !Task.isCancelled else { return false }
         self.loginPhase = .idle
         self.presentClaudeLoginResult(result)
         let outcome = self.describe(result.outcome)
         let length = result.output.count
         self.loginLogger.info("Claude login", metadata: ["outcome": outcome, "length": "\(length)"])
         if case .success = result.outcome {
+            let metadata = self.store.metadata(for: .claude)
+            self.settings.setProviderEnabled(provider: .claude, metadata: metadata, enabled: true)
+            self.settings.claudeUsageDataSource = .oauth
             self.postLoginNotification(for: .claude)
+            return true
         }
+        return false
     }
 }
diff --git a/Sources/CodexBar/Providers/Claude/ClaudeProviderImplementation.swift b/Sources/CodexBar/Providers/Claude/ClaudeProviderImplementation.swift
index da82c17..4a10152 100644
--- a/Sources/CodexBar/Providers/Claude/ClaudeProviderImplementation.swift
+++ b/Sources/CodexBar/Providers/Claude/ClaudeProviderImplementation.swift
@@ -209,8 +209,7 @@ struct ClaudeProviderImplementation: ProviderImplementation {
 
     @MainActor
     func runLoginFlow(context: ProviderLoginContext) async -> Bool {
-        await context.controller.runClaudeLoginFlow()
-        return true
+        return await context.controller.runClaudeLoginFlow()
     }
 
     @MainActor
diff --git a/Sources/CodexBar/SettingsStore.swift b/Sources/CodexBar/SettingsStore.swift
index 6d3e76e..d26a48a 100644
--- a/Sources/CodexBar/SettingsStore.swift
+++ b/Sources/CodexBar/SettingsStore.swift
@@ -379,6 +379,9 @@ extension SettingsStore {
         self.updateProviderConfig(provider: provider) { entry in
             entry.enabled = enabled
         }
+        if !enabled, self.selectedMenuProvider == provider {
+            self.selectedMenuProvider = nil
+        }
     }
 
     func rerunProviderDetection() {
diff --git a/Tests/CodexBarTests/SettingsStoreCoverageTests.swift b/Tests/CodexBarTests/SettingsStoreCoverageTests.swift
index 0b18ad8..f69853c 100644
--- a/Tests/CodexBarTests/SettingsStoreCoverageTests.swift
+++ b/Tests/CodexBarTests/SettingsStoreCoverageTests.swift
@@ -35,6 +35,21 @@ struct SettingsStoreCoverageTests {
         #expect(enabled.contains(.codex))
     }
 
+    @Test
+    func `disabling selected menu provider clears stale menu selection`() throws {
+        let settings = Self.makeSettingsStore()
+        let metadata = ProviderRegistry.shared.metadata
+
+        try settings.setProviderEnabled(provider: .codex, metadata: #require(metadata[.codex]), enabled: true)
+        try settings.setProviderEnabled(provider: .claude, metadata: #require(metadata[.claude]), enabled: true)
+        settings.selectedMenuProvider = .claude
+
+        try settings.setProviderEnabled(provider: .claude, metadata: #require(metadata[.claude]), enabled: false)
+
+        #expect(settings.selectedMenuProvider == nil)
+        #expect(settings.enabledProvidersOrdered(metadataByProvider: metadata) == [.codex])
+    }
+
     @Test
     func `menu bar metric preferences and display modes`() {
         let settings = Self.makeSettingsStore()

Validation

Confirmed locally:

  • CodexBarCLI usage --provider claude --source oauth --format json succeeds and returns Claude usage.
  • Editing the config to enabled: true, source: oauth makes CodexBarCLI usage --status show Claude as expected.

Attempted:

swift test --filter SettingsStoreCoverageTests

The test build did not reach the patched code in my local CLI environment. It stopped while compiling the KeyboardShortcuts dependency because the active developer directory is Command Line Tools and SwiftUI preview macros were unavailable:

external macro implementation type 'PreviewsMacros.SwiftUIView' could not be found for macro 'Preview(_:body:)'; plugin for module 'PreviewsMacros' not found

So the patch above is proposed as the concrete fix direction, but I could not complete the local Swift test run without a full Xcode toolchain setup.

Suggested labels

bug, priority:medium, area:ui-ux, area:auth-keychain, provider:claude, needs-triage

Metadata

Metadata

Assignees

No one assigned

    Labels

    acceptedConfirmed backlog item or verified open bugarea:auth-keychainAuthentication, keychain, cookies, token refresh, account switchingbugSomething isn't workingpriority:highHigh priority: confirmed serious bug or blockerprovider:claudeIssue specific to Claude

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions