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:
-
SettingsStore.setProviderEnabled(...) writes enabled, but if the disabled provider is also the persisted selectedMenuProvider, that stale menu selection is left behind.
-
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
Summary
Claude can end up in a confusing, internally inconsistent state where the app has
selectedMenuProvider = claude, but~/.codexbar/config.jsonstill 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
0.23 (58)0a830cec,2026-04-26T03:42:59Z26.4.1 (25E253)2.1.123Observed state
The app defaults had Claude selected:
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:
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 --statusshowed both providers, including Claude OAuth:Why this looks like an app-side bug
There are two state-safety gaps in the current code path:
SettingsStore.setProviderEnabled(...)writesenabled, but if the disabled provider is also the persistedselectedMenuProvider, that stale menu selection is left behind.ClaudeProviderImplementation.runLoginFlow(...)currently returnstrueafterrunClaudeLoginFlow()regardless of whether login succeeded.runClaudeLoginFlow()posts the login notification on success, but it does not enable the Claude provider or persistsource = .oauthafter 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
selectedMenuProvider.source = oauth, because that is the source the login flow just repaired.selectedMenuProviderpoints 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:
selectedMenuProviderwhen disabling that provider,source = .oauthbefore refreshing.Validation
Confirmed locally:
CodexBarCLI usage --provider claude --source oauth --format jsonsucceeds and returns Claude usage.enabled: true, source: oauthmakesCodexBarCLI usage --statusshow Claude as expected.Attempted:
The test build did not reach the patched code in my local CLI environment. It stopped while compiling the
KeyboardShortcutsdependency because the active developer directory is Command Line Tools and SwiftUI preview macros were unavailable: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