Skip to content

Optional claude.ai session key: official limits without a Keychain prompt #240

Closed

Description

@LuceteYang

Problem

Two rules in docs/reference/defect-log.md ("자격증명·Keychain") corner official Claude limits:

  1. Auto-polling must never read the Claude Keychain — a no-UI query suppresses the authentication prompt but not the unlock / allow-access dialog. Measured there: SecItemCopyMatching blocking 13s mid-poll with a dialog, a few times a day around token expiry.
  2. The app must not create its own Keychain item — an app-owned item's ACL breaks on every code-signature change, and that prompt can't be suppressed (fix: 앱 keychain 캐시 제거 — 서명 변경마다 반복되던 접근허용 프롬프트 근절 #58).

So once the cached token expires and ~/.claude/.credentials.json is absent, limits freeze until the user presses Refresh limit token cache — a manual step that lands on exactly the users already seeing prompts.

The same entry names the only complete cure and marks it not adopted: Developer ID notarization, because an ACL grant only persists for a trusted signing identity. That's a paid path and a reasonable one to skip. This is for the users hitting the prompt loop meanwhile.

As a user, before I had an alternative: every Keychain access raised a password dialog, and "Always Allow" didn't hold — it returned a few times a day, and again after every app update, because the new cdhash invalidated the grant.

Proposal

An opt-in second source: a claude.ai sessionKey cookie pasted under Settings → Advanced.

  • Same 5-hour / weekly numbers from claude.ai/api/organizations/{org}/usage — same schema as api.anthropic.com/api/oauth/usage, so LimitStatus is reused unchanged and popover rows don't change shape.
  • Touches no Keychain, so auto-polling can use it — limits stay current, and there's no ACL grant to lose on the next update.
  • No key saved → nothing changes and no file is created. Chain order: session key first when present, otherwise today's OAuth path.

Prior art

Several macOS/VS Code trackers already use this exact endpoint and credential — linuxlewis/claude-usage (has a written SPEC), hamed-elfayome/Claude-Usage-Tracker, f-is-h/Usage4Claude (also surfaces 7-day Opus/Sonnet), DimaNeskorodiev/claude-limits. The approach and its local-storage model are established, not novel.

The tradeoff, up front

The key is stored as a plaintext, owner-only (0600) file in the app's Application Support directory — not encrypted.

Stating it first because it looks wrong until you follow the constraints: it cannot go in the Keychain, since an app-owned item is exactly what rule 2 forbids and would reintroduce the prompts this removes. A feature whose purpose is to stop prompts can't be built on the mechanism that causes them.

Mitigations: 0600 owner-only; the value is never rendered back (placeholder once saved); a Delete button; settings copy states plainly that it's unencrypted and that browser logout invalidates it immediately; nothing written unless a key is pasted.

If plaintext storage isn't acceptable here, that's a legitimate call — worth making before any code is written, which is why this is an issue and not a PR.

Known limitations, disclosed now

  1. A dead key answers 403, not 401. Verified by flipping one character of a stored key in a running build: usage and /api/organizations both returned 403. Since /api/organizations isn't org-scoped, a 403 there means the key can't see anything and must map to "invalid key" — otherwise it's indistinguishable from an OAuth 401/403 and the user gets the wrong remedy. Per-org 403 keeps its existing meaning and triggers re-selection.
    curl cannot verify these endpoints — Cloudflare answers it with its own 403 challenge regardless of cookie, so it will confirm any theory you bring it. Only the app's URLSession gives a real answer.
  2. A chained fallback can hide a dead key. If the key dies while OAuth still works, limits keep flowing and the user is never told. Fixing it means plumbing the primary failure up instead of swallowing it — can be in scope or a follow-up.
  3. Multi-org accounts need one picked. Auto-selecting the org with usage works; a user with live personal and work orgs may want to switch.

Open questions

  1. Is plaintext file storage acceptable at all? Everything else depends on this.
  2. Org picker in the first cut, or follow-up?
  3. Should limitation 2 be in the first PR, or landed separately to keep it reviewable?
  4. Placement — Advanced disclosure seems right for an escape hatch. Happy to move it.

Running locally since 21 August; the expiry path I exercised deliberately via the single-character edit above. Happy to open the PR once the shape is agreed, in whatever slice you prefer.

Activity

  1. chattymin commented on Aug 30, 2026

    @chattymin
    Owner

    Implemented in #241, now on main.

    Landed as proposed: opt-in, session key first with the existing OAuth path as fallback, and nothing changes for users who don't save a key. Two things were added on merge:

    • claude.ai is now listed in the outbound-host section of the README (en/ko/ja). The privacy section enumerates every host the app talks to, so a new one has to appear there.
    • A configured-but-rejected key no longer lets the fallback open the Keychain. It used to pass allowKeychainPrompt straight through, so pressing Refresh with a dead key raised the very dialog the key exists to avoid. Users with no key keep today's behaviour, and both directions are locked by tests.

    On the tradeoff you raised up front — plaintext storage — the deciding factor was that the app already reads Claude credentials on the same machine, the key is opt-in and revocable by signing out of the browser, and declining wouldn't stop anyone from pasting the same cookie into one of the other tools that do this less carefully. Notarization remains the complete fix and remains unadopted for cost reasons.

    Thanks for writing the proposal before the code, and for saying plainly which parts you were unsure about. That made this easy to decide.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions