Skip to content

[Bug]: Windows: Claude usage shows "OAuth access token has expired. Re-authenticate" — Orca never refreshes the managed token (refresh token still valid, no live PTY, no recovery path on win32) #18463

Open

Description

@Minidoracat

Operating system

Windows

Orca version

1.4.195 (Windows 11 Pro 24H2, x64)

Details

Short summary

On Windows, Orca's status-bar Claude usage meter reports OAuth access token has expired. Re-authenticate to continue. even though the managed account's refresh token is still valid for weeks. Orca simply never refreshed the access token: both managed accounts were last refreshed at 09:29, the access tokens expired at 17:29 (8h TTL), and at 19:20 Orca was still running, still holding the expired tokens, with no live Claude PTY. Meanwhile a separate OAuth session for the same Anthropic account, kept by another tool with its own token store, was refreshed on schedule and returned 200 from the same usage endpoint the whole time — so the account itself is healthy.

This is distinct from #9582 / #13165 (macOS, multi-store clobber, token family revoked). No clobber happened here: ~/.claude/.credentials.json and the managed .credentials.json were byte-identical and both untouched since 09:29. The token is not broken, it is just never refreshed, and on Windows there is no recovery path after the resulting 401.

What happened (evidence, all timestamps local, today)

managed account A managed account B
.credentials.json mtime (%APPDATA%\orca\claude-accounts\<id>\auth\) 09:29:18 09:29:04
claudeAiOauth.expiresAt 17:29:16 17:29:02
claudeAiOauth.refreshTokenExpiresAt ~4 weeks out ~4 weeks out
GET https://api.anthropic.com/api/oauth/usage with that access token at 19:21 401 {"type":"authentication_error","message":"OAuth access token has expired. Re-authenticate to continue."} same 401
same account, independent OAuth session held by another local tool (own refresh token, refreshed at 19:17–19:22) 200, 5h/7d utilization returned 200

Other state at the time:

  • Orca.exe running (6 processes), main window in use (this was captured from a terminal inside Orca).
  • orca-data.json → claudeLivePtySessionIds: []; no claude / claude.exe process on the machine (only non-Claude agents in panes). So hasLiveClaudePtys() was false and the "wait for the live Claude terminal to rotate" deferral does not apply.
  • settings.activeClaudeManagedAccountId = account B; ~/.claude/.credentials.json is byte-identical to account B's managed file (Orca's materialized copy), mtime 09:29:25 — nothing else wrote it all day.
  • daemon.log and main.trace.ndjson* contain zero auth/refresh/usage entries, so from the filesystem it is impossible to tell whether the proactive refresh was never attempted or attempted and failed.

Why this can happen (reading origin/main today)

  1. The only proactive refresh is refreshManagedAccountTokenIfNeeded inside doSyncForCurrentSelection (src/main/claude-accounts/runtime-auth/runtime-auth-sync.ts:244-257). Sync runs on startup, account switch, prepareForClaudeLaunch, and prepareForRateLimitFetch. If none of those fire for 8h, the token expires.
  2. Background polling is focus-gated: service-polling.ts:60-69 shouldBackgroundPoll() requires isVisible() && !isMinimized() && isFocused(), 15-minute interval. Working in another window all afternoon means no fetch, no sync, no refresh. ([Bug]: Usage status bar stops auto-refreshing after an update relaunch (focus-gated poll), separate from the #5355 auth path #5627 was closed, but this gate is still there.)
  3. refreshClaudeOauthCredentials (src/main/claude-accounts/oauth-refresh.ts:141-170) is best-effort: any non-2xx or network error is console.warn + return null, the caller keeps the expired blob, and nothing records the failure in usageMetadata. On Windows console.warn from the main process goes nowhere the user can see.
  4. Windows has no recovery path after the 401. service-fetch-targets.ts:133-146 returns false for shouldAllowClaudePtyFallback and shouldAllowClaudeUsagePanelSupplement on win32. In claude-active-usage-fetch.ts:98-118 both repairClaudeCredentialsThenRetryOAuth and fetchClaudeUsageViaCli are gated on allowCliFallback, so on Windows a stale-token classification goes straight to the error result. On macOS/Linux the PTY fallback would let the CLI rotate the token; on Windows the meter just says "Re-authenticate" while the refresh token is perfectly usable.

Net effect on Windows: Orca is the sole owner of this token lineage (it materializes it into ~/.claude itself), yet it only refreshes opportunistically, fails silently, and has no fallback. The "re-authenticate" prompt is misleading — a plain grant_type=refresh_token call would have fixed it.

How to reproduce

  1. Windows, add a managed Claude account (subscription OAuth) and make it active.
  2. Do not open a claude pane in Orca. Keep Orca running but work in other applications (or otherwise avoid the focus-gated poll) for > 8 hours.
  3. Come back: the Claude usage chip shows OAuth access token has expired. Re-authenticate to continue.
  4. Check %APPDATA%\orca\claude-accounts\<id>\auth\.credentials.json: expiresAt is in the past, refreshTokenExpiresAt is weeks away.

Expected

  • With a valid refresh token and no live Claude PTY, Orca should refresh the access token before/at expiry regardless of window focus, or at least on the first usage fetch that returns 401 — the pure-HTTP refreshClaudeOauthCredentials already exists and needs no PTY, so the Windows allowCliFallback gate should not block it.
  • A failed refresh should surface its HTTP status (or "network") in usageMetadata / the chip instead of collapsing into "Re-authenticate".

Related

Happy to open a PR that adds a PTY-free delegated refresh path (call refreshClaudeOauthCredentials on the managed credentials, persist via writeClaudeManagedCredentialsJson, retry the OAuth usage request) and records refresh failures in usageMetadata, if maintainers are open to it given the pending auth PRs (#9754, #6864).

Activity

  1. Minidoracat commented on Sep 3, 2026

    @Minidoracat
    ContributorAuthor

    Follow-up / workaround (confirms the token itself is fine)

    I verified the failure is only in Orca's refresh path, not the account: feeding Orca through its existing live-session channel makes the meter recover instantly, with no login.

    POST http://127.0.0.1:$ORCA_AGENT_HOOK_PORT/statusline/claude (form-encoded, X-Orca-Agent-Hook-Token, fields paneKey, configDir=, env, version, payload=<JSON with rate_limits.five_hour / seven_day>) — i.e. exactly what the managed Claude Code statusLine script sends — returns 204, ingestLiveClaudeRateLimits accepts it, and the chip goes from "Re-authenticate" to live 5h/7d percentages. Because isLiveClaudeUsageFresh short-circuits the automated fetch for 5 minutes, posting every ~4 minutes keeps the bar alive indefinitely. It also clears the rate-limited state Orca gets into after opening the account switcher (each inactive-account preview on Windows fires the usage endpoint with that account's expired managed token → 401s, then 429), since the live-session ingest is not gated by retryAtMs.

    What I'm running as a stopgap: a small extension in another local agent that already holds an independent OAuth session for the same account (own refresh token, refreshed with the usual 5-minute skew). Every 4 minutes it calls /api/oauth/usage with that token and posts the result to the endpoint above. No writes to Orca's managed credentials or ~/.claude, so none of the #9582 / #13165 clobber risk.

    Two observations from doing this that may help the fix:

    • The pure-HTTP refreshClaudeOauthCredentials is all that is needed on Windows; nothing in the recovery path actually requires a PTY for a managed account.
    • The inactive-account previews (fetchInactiveClaudeAccountsOnOpen) are a cheap way to burn the usage endpoint's rate limit on Windows: they use each account's never-refreshed managed token, so every open of the switcher is N guaranteed 401s. Gating them on expiresAt (or refreshing first) would avoid the 429 → "Claude usage is rate limited right now" that follows.
  2. added theissue type on Sep 19, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions