From a36e41b92f8e8367a6f3f3714372e380fe3a7da2 Mon Sep 17 00:00:00 2001 From: Erik Josephson Date: Tue, 24 Mar 2026 05:50:55 -0400 Subject: [PATCH] fix: handle errSecInteractionNotAllowed in KeychainCacheStore to prevent cache self-destruction on wake MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When the keychain is temporarily locked (e.g. immediately after wake from sleep), SecItemCopyMatching returns errSecInteractionNotAllowed (-25308). Previously this fell into the default case, returned .invalid, and the caller deleted the cache entry — causing every wake from sleep to require a fresh read of "Claude Code-credentials", which triggers a keychain prompt. Two changes: 1. Apply KeychainNoUIQuery to the cache load query so the call never blocks waiting for UI interaction (consistent with how other no-UI reads are done elsewhere in the codebase). 2. Add an explicit case for errSecInteractionNotAllowed that returns .missing instead of .invalid — the entry is valid, just temporarily inaccessible, so it should not be deleted. --- Sources/CodexBarCore/KeychainCacheStore.swift | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/Sources/CodexBarCore/KeychainCacheStore.swift b/Sources/CodexBarCore/KeychainCacheStore.swift index e77ebbd77..a30ea3eca 100644 --- a/Sources/CodexBarCore/KeychainCacheStore.swift +++ b/Sources/CodexBarCore/KeychainCacheStore.swift @@ -46,13 +46,14 @@ public enum KeychainCacheStore { return testResult } #if os(macOS) - let query: [String: Any] = [ + var query: [String: Any] = [ kSecClass as String: kSecClassGenericPassword, kSecAttrService as String: self.serviceName, kSecAttrAccount as String: key.account, kSecMatchLimit as String: kSecMatchLimitOne, kSecReturnData as String: true, ] + KeychainNoUIQuery.apply(to: &query) var result: AnyObject? let status = SecItemCopyMatching(query as CFDictionary, &result) @@ -70,6 +71,12 @@ public enum KeychainCacheStore { return .found(decoded) case errSecItemNotFound: return .missing + case errSecInteractionNotAllowed: + // Keychain is temporarily locked (e.g. immediately after wake from sleep). + // The cache entry is valid — treat as missing so the caller falls through + // gracefully rather than deleting a perfectly good cache entry. + self.log.info("Keychain cache temporarily locked (\(key.account)), will retry on next access") + return .missing default: self.log.error("Keychain cache read failed (\(key.account)): \(status)") return .invalid