Skip to content

Cache OAuth token in app-owned Keychain#16

Open
dmelo wants to merge 4 commits intomainfrom
feat/cache-token-keychain
Open

Cache OAuth token in app-owned Keychain#16
dmelo wants to merge 4 commits intomainfrom
feat/cache-token-keychain

Conversation

@dmelo
Copy link
Copy Markdown
Owner

@dmelo dmelo commented Mar 12, 2026

Summary

  • Cache the extracted OAuth access token in a ClaudeCodeStats-credentials Keychain item owned by the app, avoiding repeated macOS authorization prompts from reading Claude Code's Keychain item
  • Add in-memory token cache to skip all I/O on subsequent polls within the same session
  • Invalidate both caches on 401/403 so the next fetch re-reads from the source

Test plan

  • Build and install locally
  • Verify first launch reads from Claude Code's Keychain (one-time prompt) and caches to app Keychain
  • Verify subsequent polls use cached token with no prompt
  • Verify 401/403 clears cache and re-prompts on next fetch

🤖 Generated with Claude Code

Reads from Claude Code's Keychain item trigger a macOS authorization
dialog on every poll. Cache the extracted token in a ClaudeCodeStats-
owned Keychain item and in memory. Invalidate both on 401/403.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings March 12, 2026 13:33
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR reduces repeated macOS Keychain authorization prompts by caching Claude Code’s OAuth access token in an app-owned Keychain item and also caching it in-memory for the current session, with cache invalidation on 401/403 responses.

Changes:

  • Add an in-memory access token cache to avoid repeated file/Keychain reads during a session.
  • Add an app-owned Keychain item (ClaudeCodeStats-credentials) to persist the token and avoid repeatedly reading Claude Code’s Keychain item.
  • Clear both the in-memory and app Keychain caches when the API returns 401/403.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

You can also share your feedback on Copilot code review. Take the survey.

- Add @mainactor to OAuthUsageService for thread safety on cachedToken
- Log failures on SecItemAdd and SecItemDelete instead of ignoring status
- Remove side effects from hasCredentials (no longer mutates cache or writes to Keychain)
- Add comment clarifying why readTokenFromAppKeychain is separate from readTokenFromKeychain

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@dmelo dmelo requested a review from Copilot March 19, 2026 11:48
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 1 out of 1 changed files in this pull request and generated 2 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

You can also share your feedback on Copilot code review. Take the survey.

…nique Keychain addressing

- hasCredentials now delegates to readAccessToken() so the in-memory
  cache is populated on first check, avoiding repeated file/Keychain I/O
- Add fixed kSecAttrAccount ("oauth-token") to all app-Keychain queries
  so the cached token item is uniquely addressed

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 1 out of 1 changed files in this pull request and generated 2 comments.

Comments suppressed due to low confidence (1)

ClaudeCodeStats/ClaudeCodeStats/Services/OAuthUsageService.swift:22

  • @MainActor on the whole service forces all synchronous file/keychain I/O in readAccessToken() (and any hasCredentials evaluation) to run on the main actor. FileManager.default.contents(atPath:) and SecItemCopyMatching can block, which may cause UI hitching during SwiftUI body evaluation or refresh. Consider removing the class-level @MainActor (or narrowing actor isolation) and performing the keychain/file reads off the main thread while keeping only UI-facing state updates on the main actor.
@MainActor
class OAuthUsageService {
    static let shared = OAuthUsageService()

    private let apiURL = "https://api.anthropic.com/v1/messages"
    private let credentialsPath: String = {
        let home = FileManager.default.homeDirectoryForCurrentUser.path
        return "\(home)/.claude/.credentials.json"
    }()
    private let keychainService = "Claude Code-credentials"
    private let appKeychainService = "ClaudeCodeStats-credentials"
    private let appKeychainAccount = "oauth-token"
    private var cachedToken: String?

    private init() {}

    var hasCredentials: Bool {
        readAccessToken() != nil
    }

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

- Rename reflects that both in-memory and Keychain caches are cleared
- Set kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly to prevent
  iCloud Keychain sync and restrict access appropriately

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants