Repository navigation
feat: fetch official limits with a claude.ai session key (no Keychain prompt) - #1
Merged
Merged
Conversation
… prompt)
The Keychain path raises a macOS password dialog because the app is not in the
ACL of Claude Code's credential item, and a self-signed signature invalidates
"Always Allow" on every rebuild. Auto-polling therefore never read the Keychain,
so limits went stale until the user pressed refresh.
Add a second credential path: paste the claude.ai `sessionKey` cookie in
Settings and limits are fetched from `claude.ai/api/organizations/{org}/usage`,
which returns the same schema as `api.anthropic.com/api/oauth/usage` — so
`LimitStatus` is reused as-is. The path touches no Keychain, so auto-polling
keeps limits current. Without a key, behaviour is unchanged: the chain falls
back to the existing OAuth path.
Organization selection is not "the first one". A claude.ai account also returns
orgs the user only logged into and API-only orgs that answer 403, so picking
first yields a permanent, plausible-looking 0%. Candidates are filtered to
`chat`-capable orgs, probed in parallel, and the one with usage is selected and
cached; a cached org that later returns 403 triggers rediscovery, while 401
(dead key) does not.
The key is stored as an owner-only (0600) plaintext file, not in the Keychain:
an app-owned Keychain item is the very class of defect this feature removes
(cdhash changes invalidate the ACL and the prompts return).
Also consolidate PTB_STATE_DIR resolution into AppIdentity.stateDirectory —
the architecture guard that forbids reading usage-location env vars directly
caught the new store, and two readers of the same override would have left QA
isolation half-applied.
Verified live: the app discovers organizations against claude.ai and persists
the one with usage. The endpoint cannot be checked with curl (Cloudflare
challenges it; only URLSession passes), so `scripts/probe-session-key.swift`
covers that gap and the unit tests seam out the HTTP layer.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
The official-limits path reads Claude Code's credential from the Keychain in-process. The app is not in that item's ACL, so macOS raises a password dialog, and a self-signed signature invalidates "Always Allow" on every rebuild. Auto-polling was therefore forbidden from reading the Keychain at all (chattymin#58 class), which left limits stale until the user pressed refresh.
What
A second credential path, modelled on hamed-elfayome/Claude-Usage-Tracker: paste the claude.ai
sessionKeycookie in Settings → Advanced.claude.ai/api/organizations/{org}/usagereturns the same schema asapi.anthropic.com/api/oauth/usage, soLimitStatusis reused unchanged.ChainedLimitsProvider: session key first, existing OAuth path as fallback. Users who save no key see no change.disableKeychainAccessno longer suppresses limits when a session key is configured — that toggle means "no Keychain pop-ups", not "no limits".Organization selection is the real risk
A claude.ai account returns more than the org you use: an org you only logged into (0%,
resets_at: null) and API-only orgs that answer 403. The reference implementation'sorganizations.first!therefore reports a permanent 0% on such an account — and crashes on an empty array. Instead: keepchat-capable orgs, probe them in parallel, select the one with usage, cache it. A cached org that starts returning 403 triggers rediscovery; 401 (dead key) does not.Storage
Owner-only (
0600) plaintext file under Application Support — not a Keychain item, because an app-owned item reintroduces exactly the prompts this feature removes. The Settings row states the trade-off and how to revoke (log out in the browser). README privacy sections updated in all three languages.Verification
firstlogic fails the org-selection tests, removing either permission mechanism fails the0600test, and reverting the toggle gate fails the new store test.403 cf-mitigated: challengewhile URLSession gets 200.scripts/probe-session-key.swiftcovers that gap; unit tests seam out the HTTP layer. Recorded indocs/reference/defect-log.mdalong with theorganizations.firstclass and the plaintext-storage rationale.🤖 Generated with Claude Code