diff --git a/CHANGELOG.md b/CHANGELOG.md index 053cc0ef21..5ec04ef4cc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ - Kimi: reuse fresh signed-in Kimi Code CLI credentials in Auto mode without refreshing or rewriting CLI-owned authentication state. Thanks @Leechael! ### Fixed +- Claude: keep MCP-only credential discovery out of background Security.framework reads when Keychain prompts are limited to user actions, preventing recurring macOS password dialogs (#2115). - Claude: cache successful CLI version probes for 30 minutes while invalidating on executable changes, avoiding repeated PTY launches without retaining failed or stale wrapper results. Thanks @Yuxin-Qiao! - Linux CLI: bootstrap the configured IANA timezone before Foundation startup on non-FHS systems, preventing SIGILL on NixOS (#2127). Thanks @xikhar! - Ollama: release temporary dashboard network sessions after each fetch, preventing repeated refreshes from retaining delegates and URL-cache resources. Thanks @astuteprogrammer! diff --git a/Sources/CodexBarCore/Providers/Claude/ClaudeOAuth/ClaudeOAuthCredentials+SecurityCLIReader.swift b/Sources/CodexBarCore/Providers/Claude/ClaudeOAuth/ClaudeOAuthCredentials+SecurityCLIReader.swift index 76df9cb2c6..54eedb7007 100644 --- a/Sources/CodexBarCore/Providers/Claude/ClaudeOAuth/ClaudeOAuthCredentials+SecurityCLIReader.swift +++ b/Sources/CodexBarCore/Providers/Claude/ClaudeOAuth/ClaudeOAuthCredentials+SecurityCLIReader.swift @@ -357,7 +357,17 @@ extension ClaudeOAuthCredentialsStore { guard !keychainAccessDisabled || self.isolatedSecurityCLIKeychainPath(environment: environment) != nil else { return false } - guard ClaudeOAuthKeychainPromptPreference.effectiveMode(readStrategy: readStrategy) != .never else { + let promptMode = ClaudeOAuthKeychainPromptPreference.effectiveMode(readStrategy: readStrategy) + guard promptMode != .never else { + return false + } + // Security.framework "no UI" queries can still display the legacy Keychain ACL password dialog on some + // systems. Do not touch the Claude Code keychain from background discovery when the stored policy only + // permits user-initiated access. Explicit refreshes retain MCP-only detection, and `.always` remains opt-in. + if readStrategy == .securityFramework, + promptMode == .onlyOnUserAction, + interaction != .userInitiated + { return false } let payload: Data? = switch readStrategy { diff --git a/Tests/CodexBarTests/ClaudeOAuthCredentialsStoreMCPOnlyGuardTests.swift b/Tests/CodexBarTests/ClaudeOAuthCredentialsStoreMCPOnlyGuardTests.swift index 36ea589e94..0c129f2291 100644 --- a/Tests/CodexBarTests/ClaudeOAuthCredentialsStoreMCPOnlyGuardTests.swift +++ b/Tests/CodexBarTests/ClaudeOAuthCredentialsStoreMCPOnlyGuardTests.swift @@ -6,7 +6,7 @@ import Testing @Suite(.serialized) struct ClaudeOAuthCredentialsStoreMCPOnlyGuardTests { @Test - func `standard reader blocks expired CLI owner in background but preserves user refresh`() async throws { + func `standard reader skips MCP keychain probe in background but preserves user refresh`() async throws { let service = "com.steipete.codexbar.cache.tests.\(UUID().uuidString)" let mcpOAuthOnly = Data(#"{"mcpOAuth":{"plugin:test":{"accessToken":"synthetic"}}}"#.utf8) let tempDir = FileManager.default.temporaryDirectory @@ -37,7 +37,17 @@ struct ClaudeOAuthCredentialsStoreMCPOnlyGuardTests { keychainAccessDisabled: false, environment: [:]) } - #expect(isMcpOnly) + #expect(!isMcpOnly) + + let userInitiatedIsMcpOnly = ProviderInteractionContext.$current + .withValue(.userInitiated) { + ClaudeOAuthCredentialsStore.isMcpOAuthOnlyClaudeKeychainPayloadPresent( + interaction: .userInitiated, + readStrategy: .securityFramework, + keychainAccessDisabled: false, + environment: [:]) + } + #expect(userInitiatedIsMcpOnly) ClaudeOAuthCredentialsStore.invalidateCache() let cacheKey = KeychainCacheStore.Key.oauth(provider: .claude) @@ -54,10 +64,10 @@ struct ClaudeOAuthCredentialsStoreMCPOnlyGuardTests { environment: [:], allowKeychainPrompt: false, respectKeychainPromptCooldown: true) - Issue.record("Expected MCP-only keychain failure") + Issue.record("Expected background refresh delegation") } catch let error as ClaudeOAuthCredentialsError { - guard case .mcpOAuthOnlyKeychain = error else { - Issue.record("Expected .mcpOAuthOnlyKeychain, got \(error)") + guard case .refreshDelegatedToClaudeCLI = error else { + Issue.record("Expected .refreshDelegatedToClaudeCLI, got \(error)") return } } catch {