Skip to content

fix(rate-limits): stop driving a hidden Claude to read usage - #24302

Closed
brennanb2025 wants to merge 5 commits into
mainfrom
brennanb2025/claude-usage-no-blind-keys
Closed

brennanb2025 wants to merge 5 commits into
mainfrom
brennanb2025/claude-usage-no-blind-keys

Conversation

@brennanb2025

@brennanb2025 brennanb2025 commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor
Files Added Deleted Net
Test 18 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​804 $\color{#cf222e}{\Huge{\mathbf{−}}}$​1660 $\color{#cf222e}{\Huge{\mathbf{−}}}$​856
Prod 40 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​512 $\color{#cf222e}{\Huge{\mathbf{−}}}$​1461 $\color{#cf222e}{\Huge{\mathbf{−}}}$​949

Fixes: Orca's background Claude usage check could start a hidden interactive Claude and press keys without looking at the screen: Enter every 0.8 s, and y on anything that looked like a trust prompt. On a login screen, that Enter starts a browser sign-in nobody asked for. Same bug class as #17415 (fixed for Codex in #23806).

Scope: the Claude usage meter (status bar, usage menu, account previews). The hidden Claude only ever ran on macOS and Linux, so that is where the behavior change is. On Windows, one wording change applies too (see "Signed-out accounts" below). Terminals, chats and every other provider are not affected.

ELI5

To read your Claude plan usage, Orca sometimes opened a hidden Claude window, typed /usage, and kept pressing Enter blindly. If Claude was showing something else, like a login picker, those keypresses chose options for you. Orca now never opens Claude's interactive screen. It reads usage straight from Anthropic. When an Orca-managed account's login has expired and Orca's own renewal didn't fix it, Orca starts Claude in its headless mode, asks it one question so it renews its own login, and closes it. If that doesn't work either, the meter now tells you to renew the login instead of saying "Refreshing sign-in" forever.

What Changed

Before: the hidden interactive Claude ran in four situations:

  1. Fable fill-in. The usage reply had no Fable weekly window. This ran on every poll for every macOS/Linux user, including people signed in with their own Claude, so their real Claude setup and its hooks ran in the background.
  2. Inactive managed accounts. The same Fable fill-in ran for account previews, after briefly copying that account's login into the Keychain.
  3. The usage request failed with a server error, a network error, a Keychain Orca couldn't read, or no saved login at all. With no login, the hidden Claude opens on its login picker, and the blind Enter picks "Claude account with subscription".
  4. Expired login on the active Orca-managed account. Starting Claude made it renew the login.

After:

  • Situations 1–3 no longer start anything. The Fable row comes from the same limits list Claude's own /usage panel reads, which Orca already parses. Server, network and Keychain failures show the error, and the last good numbers stay for up to 30 minutes.
  • Signed-out accounts. A managed account with no saved login now shows "Signed out" in the status bar, and the tooltip says "This Claude account is signed out. Re-authenticate it in Settings → AI Provider Accounts to see its usage." Before, it said "No subscription plan — API key billing" and hid the meter. This applies on Windows too, including WSL managed accounts: there the meter used to disappear, and now it stays visible with the signed-out state. A user signed in with their own Claude, or using an API key, keeps today's wording.
  • Situation 4 (Orca-managed account, expired login, no Claude running on it, not Windows). Every usage poll for a managed account already starts with Orca's own renewal: when no Claude is running and the token is about to expire, Orca renews it itself and saves it (this PR doesn't change that). The headless Claude is the fallback for when that renewal didn't produce a working login:
    • Orca starts Claude in its headless stream mode through the same connection structured chat uses, never sends a message, and asks for plan usage with a get_usage request. Claude renews its own login under its own refresh lock and saves it to its own store.
    • Orca ignores Claude's reply, because it can be a cached reading or empty after a network error. It re-reads the stored login and asks the usage endpoint itself.
    • The child runs in an empty Orca-owned folder, with hooks, MCP servers and project settings off and no transcript saved. It gets the user's proxy, and it counts as a live Claude for Orca's existing "don't renew while Claude runs" gate.
    • Account switches. All logins share one Claude home, so a switch that lands while this runs would point Claude at a different login, possibly the user's own. Orca waits for a running switch to finish, then re-reads which account is selected. If it changed, or the poll was cancelled, Claude is not started.
    • Retry limits. If Claude ran and the stored login didn't change, Orca waits 15 minutes before asking Claude again. If Claude was never started (switch, cancelled poll), nothing is held back. Before starting Claude, Orca checks that the Claude it found is an executable file. If it's missing or not executable, nothing is started, the meter says Claude usage is unavailable (not "Sign-in expired", since nothing was learned about the login), and Orca checks again after 15 minutes. A Claude version without get_usage is not started again until the binary changes.
    • When Claude couldn't renew it either, the status bar says "Sign-in expired" and the tooltip says "Claude could not renew this account's sign-in. Start a Claude session on it to renew, or re-authenticate it in Settings → AI Provider Accounts." Before, it said "Refreshing sign-in" indefinitely, even though nothing was refreshing. The wording doesn't claim the login is revoked, because a network failure inside Claude looks the same from Orca's side.

Deleted: the hidden-terminal probe (claude-pty.ts), its screen parsers and stop markers, the Fable and preview fill-ins, the preview Keychain staging, the "oauth then cli" refresh plan, and the hidden-terminal cleanup registry.

Why

Reading usage from a screen with blind keypresses can't be made safe: the probe can't know which screen it's answering. Each removed fallback either gets the same data Orca already has (Claude's /usage reads the same endpoint, and reads the Keychain with the same security tool) or lands on the login picker. An expired login that Orca's own renewal couldn't fix is the one case where Claude can do something Orca can't: renew under its own lock, so it never races a Claude the user started outside Orca. So that case still asks Claude, but through its headless interface, where no prompt can appear.

Alternatives considered:

  • Rely only on Orca's own renewal. Orca already tries it first on every poll. In one check with a made-up refresh token, Anthropic's token endpoint answered 429 to the Mozilla/5.0 User-Agent Electron sends, so Orca's renewal may be failing today. That is a separate bug in existing code and gets its own follow-up. Even once fixed, Orca's renewal doesn't take Claude's refresh lock, so it can race a Claude started outside Orca.
  • Drop the expired-login case entirely. Idle managed accounts would then show "expired" until the user next runs Claude.
  • Keep the hidden window and stop the blind keys. That still reads a screen, and still loads hooks and MCP servers.

Differences from how comparable apps do this:

  • Intended: the headless Claude starts only from a usage poll that finds an Orca-managed account's login expired, at most once per 15 minutes per unchanged login, and never for a user's own login. A background meter shouldn't spawn processes routinely.
  • Intended: Orca discards Claude's get_usage answer and reads the endpoint itself, since Claude may answer from a snapshot up to 60 s old.
  • Temporary: the active managed account has two renewers: Orca's own renewal, which runs first on each poll and ignores Claude's lock, and Claude through this path. This predates the PR (the hidden window was the second renewer before). Follow-up: pick a single renewal owner for the account that is currently in Claude's home.
  • Temporary: Windows is excluded by platform, not by what actually matters. The real limit is that WSL managed accounts aren't reachable by a Claude started on the Windows host. Follow-up: gate on the account's runtime (host vs. WSL) instead of the platform, so host managed accounts on Windows get the same recovery.
  • Intended: inactive managed accounts are renewed only by Orca's own renewal, as before. Orca is their only owner since they aren't in Claude's home. If that renewal fails, the preview shows the expired state until the account is used.
  • Known stale comment: config/relay-assets/node-pty-1.1.0-windows-pty-teardown-patch.cjs still names the deleted claude-pty.ts. That file is a hashed relay asset, and any byte change makes every SSH host reinstall its relay, so it's left alone. The non-hashed test comment that said the same thing is updated.

Linked Issue

Follow-up to #17415 / #23806 (same bug class, Claude instead of Codex).

Visual Proof

N/A for now: the meter's layout is unchanged. The new text is "Signed out" (managed account with no login) and "Sign-in expired" (Claude couldn't renew the login), both with tooltip guidance, and localized in every shipped language. Live Electron QA of these states is pending (see Testing).

Testing

  • Real installed Claude 2.1.280, in a throwaway home and config folder, never the real ~/.claude: the headless get_usage request was answered in about 1.2 s without any message being sent. The child exited cleanly, no transcript was written, and the transcript scan was skipped (behaviors: null). With no login it reported rate_limits_available: false, with no prompt and no error.
  • Contract pin against the real SDK and the scripted fake CLI: the runtime control request still exists and carries skip_behaviors.
  • Rate-limit, Claude connection, account-gate and status-bar copy suites (16 files, 207 tests) pass. Tests cover: the expired-login refresh and retry; the 15-minute back-off only after Claude actually ran; no back-off when Claude was never started; the unsupported-CLI skip; a missing Claude, a missing path, a folder, or a non-executable file being reported as unavailable without starting anything, with its own back-off (removing that check makes its test fail); not starting Claude when the selected account changed during or before the switch wait, or when the poll was cancelled during it (removing those checks makes these tests fail); never starting Claude for a user's own login, server errors, rate limits, an unreadable Keychain, or a usage reply with no Fable window; and the "Signed out" / "Sign-in expired" wording.
  • Main-process and renderer typechecks, oxlint, oxfmt, the localization checks and the changed-code quality gate pass.
  • Not verified live: Claude actually renewing an expired login through this path, since that needs a real expired managed-account login. Its code path (usage read with OAuth refresh under .oauth_refresh.lock, saved by compare-and-swap) was read from the 2.1.280 binary. Also not tested live: Electron UI, Linux, WSL, SSH.

Notes

  • Security: removes a background process that could answer prompts on the user's behalf. The headless child gets no user message and runs with hooks and MCP off. Claude's own updater and IDE extension install live in its interactive screen, which the headless mode never shows.
  • Cross-platform: Windows never started a hidden Claude and still doesn't; the only Windows change is the "Signed out" state above. Linux follows macOS.
  • SSH: usage is polled locally; unaffected.
  • Compatibility: no wire or persisted-field change. Claude now also reports the existing delegated-refresh-required failure kind (already used by Grok and Kimi). An older Orca window that reads it shows "Refreshing sign-in", which is what it showed before. Mobile and the CLI don't read failure kinds. The 'cli' source value is kept.
  • Performance: a failed refresh no longer holds a hidden terminal for up to 25 s. The headless probe takes about 1–2 s and runs at most once per 15 minutes per unchanged login.

Checklist

  • This PR is small and focused
  • I explained what changed and why (ELI5, the user-facing before/after, the mechanism, and why over the alternatives)
  • Before/after screenshots or videos attached for UI changes, or N/A with reason
  • Self-reviewed for correctness, security, and performance
  • Cross-platform, SSH/remote, and path/shortcut impact considered (or N/A)
  • pnpm lint, pnpm typecheck, pnpm test, and pnpm build pass (or CI will cover; local preferred)

Claude usage fell back to a hidden interactive `claude` that typed /usage,
pressed Enter every 0.8 s without reading the screen and answered "y" to
anything that looked like a trust prompt. On a login picker that Enter
starts a browser sign-in. It ran for system-default logins too: whenever
the usage reply had no Fable window, on every poll.

Every fallback whose result could not differ from the direct usage read is
gone (Fable fill-in, inactive-account preview fill-in, server/network,
unreadable Keychain, no credentials), together with the hidden PTY, its
screen parsers and the preview Keychain staging. The one case that needs
Claude itself, an expired login on an Orca-managed account with no Claude
running, now starts Claude headless, never sends a message, and asks for
plan usage so Claude refreshes its own login under its own lock. Orca
ignores that answer, re-reads the stored login and asks the usage endpoint
itself; an unchanged login backs off for 15 minutes and a CLI without
get_usage is not started again until the binary changes. A managed account
with no login now reports itself as signed out.
… and say when a login needs the user

- Waiting out an account switch no longer starts Claude on whatever login the switch left in
  the shared Claude home (possibly the user's own): the refresh re-reads the selected account,
  bails on a change or an abort, and never starts Claude.
- The 15-minute backoff is only set when Claude actually ran; a refresh Orca never started
  backs nothing off.
- After Claude ran and the stored login is still expired, the meter says "Sign-in expired" and
  how to renew, instead of "Refreshing sign-in" forever.
- A managed account with no login says "Signed out" and points to re-authentication, instead
  of "Refresh failed".
…ame the real Settings page

- A Claude binary that is missing or can't be executed fails asynchronously, after the
  connection opens. The refresh now reads the connection's processless verdict and reports
  "not launched" instead of a failed login refresh: the meter says usage is unavailable rather
  than "Sign-in expired", and a 15-minute per-binary backoff stops a spawn attempt every poll.
- The signed-out and renewal tooltips point to Settings → AI Provider Accounts (the page's real
  title, localized from the existing sidebar translation) instead of a nonexistent "Accounts".
- Fix a stale comment that still described the removed hidden-terminal gate.
…h launches it

On macOS and Linux the launch goes through a supervisor process that always starts, so a
missing Claude binary only showed up as the supervisor exiting, never as the "processless"
verdict the previous fix read. The refresh now checks the resolved command before launching:
a bare name (the resolver found nothing) or a path that is not an executable file is reported
as not launched, without spawning anything, and keeps the per-binary backoff and the
"usage unavailable" reporting. The after-the-fact verdict read and its extra close() are gone,
so each connection is closed exactly once.
@brennanb2025

brennanb2025 commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor Author

Review status: ready for a human look, still a draft

The problem. To read Claude plan usage, Orca could start a hidden interactive claude, type /usage, press Enter every 0.8 s without reading the screen, and answer y to anything that looked like a trust prompt. On a login picker, that Enter starts a browser sign-in nobody asked for. It ran on macOS and Linux, including for people signed in with their own Claude, on every poll where the usage reply had no Fable window.

What changes for users.

  • Orca never opens Claude's interactive screen to read usage. The meter reads usage straight from Anthropic, as before.
  • The only time Orca still starts Claude is when an Orca-managed account's login has expired and Orca's own renewal didn't fix it (not on Windows). It starts Claude headless, sends no message, asks for usage once, and closes it. At most once per 15 minutes per unchanged login.
  • A managed account with no login now shows "Signed out", with a pointer to Settings → AI Provider Accounts. Before, it said "No subscription plan — API key billing".
  • A managed login that Claude couldn't renew now shows "Sign-in expired", saying how to fix it. Before, "Refreshing sign-in" stayed up forever.
  • People on their own Claude login or an API key see the same wording as before.

Fixed during review (head 91aa11640ba):

  • After an account switch, the headless Claude could start on whichever login was now selected, including the user's own. It now re-checks the selected account after the switch settles and doesn't start if it changed.
  • A cancelled poll could still start Claude after waiting out an account switch.
  • The 15-minute back-off was set even when Claude never ran. It's now set only after Claude actually ran.
  • The signed-out state showed a generic "Refresh failed".
  • A dead managed login said "Refreshing sign-in" forever.
  • A missing Claude binary was reported as "Sign-in expired". It's now checked before launch and shows "Usage unavailable", backed off per binary.
  • PR description corrections:
    • Orca already renews managed-account logins itself on every poll; the headless Claude is the fallback.
    • Windows managed accounts with no login now show "Signed out".
    • The Windows exclusion is labelled temporary.

Deferred

  • Follow-ups for existing behavior, none caused by this PR:
    • Orca's own token renewal sends Electron's browser User-Agent. The PR author saw the token endpoint answer 429 to that, so Orca's renewal may be failing today; not verified independently.
    • Orca and Claude both renew the same single-use refresh token.
    • An account switch landing during a renewal can overwrite the renewed login (same race as any live Claude terminal).
    • If usage is rate-limited right after a successful renewal, the meter shows "Refreshing sign-in" instead of "Limited".
  • Labelled temporary in the description: the Windows exclusion should be "host vs. WSL account", not "Windows".
  • Nits:
    • The hidden-rate-limit-pty-cwd module name.
    • The Claude "Sign-in expired" label shares MiniMax's translation key.
    • Stale CI timing entries for deleted tests.

Verified

  • Targeted unit tests for the refresh path, the back-offs, account-switch re-check, cancel, missing binary, and the new status-bar copy pass. The account-switch, cancel and missing-binary tests were confirmed to fail with their guard removed.
  • Main and renderer typechecks, lint, the changed-code quality gate and the localization checks pass.
  • The installed Claude 2.1.261 and 2.1.280 binaries (read as text, not run) accept get_usage with skip_behaviors. They renew the login on a 401 in that path, and the "unsupported" error text matches what Orca detects.
  • Live Electron run on a separate Mac, built from 91aa11640ba, isolated home and profile:
    • With no Claude login, the meter matches main and Orca made no Claude attempt.
    • Across 4 usage refreshes, the app started no claude process and no sign-in window opened.
    • "AI Provider Accounts" is the real settings page name.
Screenshot Shows
Status bar, own account with no login: no Claude item, same as main
Usage popover: no Claude row
Settings → AI Provider Accounts

Not verified

  • Live managed-account states ("Signed out", "Sign-in expired") and the headless renewal.
    • Mac: adding or switching a managed account writes the real Claude Code-credentials Keychain item, which a home-folder sandbox can't isolate. The QA run stopped rather than touch it.
    • Linux: the boxes weren't reachable. On Linux, credentials are files, so a sandboxed home would make this testable.
  • Claude actually renewing a real expired login through get_usage. This is the main remaining risk. If it doesn't renew, an expired managed account whose own Orca renewal also fails shows "Sign-in expired", where main's hidden window would have shown numbers.
  • Linux, WSL and SSH.

CI. static analysis is red on max-lines in src/main/native-chat/agent-session-wire/structured-agent-session-adapter.ts. This PR doesn't touch that file; it comes from main (#24235). The other checks that ran passed.

@brennanb2025

Copy link
Copy Markdown
Contributor Author

Closing as obsolete: main no longer has the code this PR changes, or already behaves as it intended. Main no longer starts a hidden Claude to read usage. 8b2a5e4 (#26801) deleted the hidden-Claude terminal and CLI usage fetch code (claude-pty.ts, claude-cli-usage-fetch.ts, claude-oauth-recovery.ts and others). Main's src/main/rate-limits has no claude-pty or CLI fetch, only the OAuth usage

(Stale-PR cleanup 2026-10-20, approved by Brennan. Reopen if something here is still missing on main.)

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