Skip to content

feat: fetch official limits with a claude.ai session key (no Keychain prompt) - #1

Merged
LuceteYang merged 1 commit into
mainfrom
feat/session-key-limits
Aug 21, 2026
Merged

LuceteYang merged 1 commit into
mainfrom
feat/session-key-limits

Conversation

@LuceteYang

Copy link
Copy Markdown
Owner

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 sessionKey cookie in Settings → Advanced.

  • claude.ai/api/organizations/{org}/usage returns the same schema as api.anthropic.com/api/oauth/usage, so LimitStatus is reused unchanged.
  • No Keychain access at all → auto-polling keeps limits current instead of going stale.
  • ChainedLimitsProvider: session key first, existing OAuth path as fallback. Users who save no key see no change.
  • disableKeychainAccess no 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's organizations.first! therefore reports a permanent 0% on such an account — and crashes on an empty array. Instead: keep chat-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

  • 814 tests green; coverage gate 90.5% (threshold 75%); no unexecuted regions in the new file.
  • Guards proven by defect injection: the reference first logic fails the org-selection tests, removing either permission mechanism fails the 0600 test, and reverting the toggle gate fails the new store test.
  • Live end-to-end: the app discovers orgs against claude.ai and persists the one with usage (reproduced 3×, including after refactors).
  • This endpoint cannot be verified with curl — Cloudflare answers curl with 403 cf-mitigated: challenge while URLSession gets 200. scripts/probe-session-key.swift covers that gap; unit tests seam out the HTTP layer. Recorded in docs/reference/defect-log.md along with the organizations.first class and the plaintext-storage rationale.

🤖 Generated with Claude Code

… 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>
@LuceteYang
LuceteYang merged commit be4f841 into main Aug 21, 2026
@LuceteYang
LuceteYang deleted the feat/session-key-limits branch August 21, 2026 13:20
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.

1 participant