Skip to content

Refresh the token like Claude Code does: current endpoint, shared lock, recover on 401 - #23

Merged
weltern merged 2 commits into
developfrom
claude/nice-carson-ek6t2l
Sep 25, 2026
Merged

weltern merged 2 commits into
developfrom
claude/nice-carson-ek6t2l

Conversation

@weltern

@weltern weltern commented Sep 25, 2026

Copy link
Copy Markdown
Owner

Why

When Claude Code runs from the desktop app, the app keeps its own login and passes the token straight to Claude Code, so nothing renews ~/.claude/.credentials.json any more; in the terminal, the claude CLI kept it fresh as a side effect. fix/token-expired-badge (already on develop) handles most of this: it refreshes 30 minutes early, backs off based on the failure's cause, stops after a rejected refresh token, and adds "Sign in again". This PR adds what that branch left out, based on reading how the current Claude Code release (2.1.282) refreshes.

Changes

  • Current token endpoint. Refreshes now go to platform.claude.com/v1/oauth/token, which the CLI uses now. console.anthropic.com is kept only as a fallback when the new address can't be reached or answers 404, 405 or a redirect. The request also carries the credentials' stored scopes and clientId, the way the CLI's does.
  • Shares Claude Code's refresh lock. Clawdmeter takes ~/.claude/.oauth_refresh.lock (the same mkdir-based lock the CLI uses, stale after 60s) plus the older ~/.claude.lock. Once it holds the lock it re-reads the file. If the access token has changed since it looked, Clawdmeter stands down, because the CLI (or another Clawdmeter) already refreshed and a refresh token only works once.
  • New RefreshOutcome.BUSY. Returned when another process holds the lock. An automatic refresh that gets BUSY stays quiet and doesn't back off.
  • Recovers from a 401 straight away. When the usage check comes back auth-expired (a 401), the poller re-reads the file and re-checks usage if the token there is newer. Otherwise it refreshes, within the usual cooldown and refresh block, and re-checks immediately instead of waiting for the next poll.
  • README: the Token section now explains the desktop-app situation.

refresh() keeps develop's contract: it always refreshes unless the token changed while it waited for the lock.

Testing

  • python -m pytest tests -q: 712 passed, 3 skipped.
  • New tests/test_token_refresh_coexist.py covers: the endpoint and scopes in the request, the fallback to the old address, a held lock (BUSY, no network call), taking over a stale lock, standing down when the token was already rotated, a rejected refresh token reporting REJECTED, a BUSY automatic refresh staying silent, and recovering from a 401 when the stored expiry looked valid.
  • Not tested against a real Anthropic login.

macOS is unchanged: the Keychain is still read-only.

🤖 Generated with Claude Code

https://claude.ai/code/session_01LsZHk76Z7ym9WENv2U2MMy


Generated by Claude Code

The Claude desktop app hands Claude Code its own token and never writes
~/.claude/.credentials.json, so the file only stays fresh while the CLI
is in use. Clawdmeter now refreshes the way the CLI does:

- refresh 5 min before expiry instead of after it lapses
- use the current token endpoint (platform.claude.com), falling back to
  the legacy console host, and send the stored scopes
- take Claude Code's .oauth_refresh.lock and re-read the file under it,
  standing down if the tokens were already rotated, so the two never
  spend the same refresh token
- on a 401 with a valid-looking expiry, re-read / force a refresh and
  re-poll right away instead of waiting out the expiry
- remember refresh tokens the endpoint rejected and stop resending them

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LsZHk76Z7ym9WENv2U2MMy
Develop's fix/token-expired-badge covers the same desktop-app diagnosis
(early refresh, RefreshOutcome backoff, rejected-token block, Sign in
again). Keep that structure and layer on what it lacked: the current
token endpoint and stored scopes, Claude Code's refresh lock with a
BUSY outcome, standing down when the tokens were rotated under us, and
an immediate refresh + re-poll on a 401. The per-run dead-token set is
dropped in favour of the poller's REJECTED block.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LsZHk76Z7ym9WENv2U2MMy
@weltern
weltern merged commit 3010b08 into develop Sep 25, 2026
9 checks passed
@weltern
weltern deleted the claude/nice-carson-ek6t2l branch September 25, 2026 04:00
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.

2 participants