Vectrum
Sign In

Design: refresh is not shipped, and why

Nine attempts at the token endpoint over ~6 minutes, every one a 429.
No exchange happened, so the refresh token was never consumed and the
user's Claude Code login is untouched.

A rejection that holds uniformly for six minutes is not throttling, and
we have seen this shape before: the Messages API also answers a request
it declines with 429 rate_limit_error rather than 401. On these OAuth
surfaces a 429 is not evidence of a rate limit.

Finding the route that does work would mean guessing at auth endpoints
while holding a live refresh token, so we stop instead. The fallback —
expired token says to run claude — is a complete feature on its own.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Jjonicommitted 1 parent 3316c66commit69c3027

Showing 1 changed file with 35 additions and 20 deletions.

+35−20docs/superpowers/specs/2026-09-17-claude-code-oauth-design.mdView File
@@ -126,26 +126,41 @@ process lifetime, and is mutex-guarded: streaming and non-streaming requests can
126126 be in flight together, and two concurrent refreshes of the same token is exactly
127127 the scenario the next section is about.
128128
129- ## Refresh, and an open question that gates it
130-
131- Refreshing is `POST /v1/oauth/token` with the refresh token. If the endpoint
132- returns a *new* refresh token, then refreshing without writing it back consumes
133- the one Claude Code still has on disk — its copy goes dead and `claude` asks you
134- to log in again. We would have broken a tool outside this repo to save a paste.
135-
136- We do not yet know whether that endpoint rotates. So the first implementation
137- task is a probe, not code: refresh once, compare the returned refresh token to
138- the stored one, report.
139-
140- - **No rotation** — in-memory-only refresh is safe. Build it as described.
141- - **Rotation** — stop. Bring the result back before writing refresh code; the
142- choice then is write-back (with atomic rename and 0600) or no refresh at all,
143- and it is not a choice to make silently inside an implementation task.
144-
145- Until that resolves, the fallback is the honest one: use the stored token, and
146- when it is expired fail with `your Claude Code login has expired, run claude to
147- refresh it`. That is a working feature on its own. Refresh is an improvement on
148- top of it, not a prerequisite.
129+ ## Refresh: not shipped, and why
130+
131+ The question was whether `POST /v1/oauth/token` rotates refresh tokens. If it
132+ does, refreshing without writing the result back consumes the token Claude Code
133+ still has on disk — its copy goes dead and `claude` asks you to log in again.
134+ We would have broken a tool outside this repo to save a paste.
135+
136+ **The probe never got an answer, and that is the answer.** Nine attempts to
137+ `https://console.anthropic.com/v1/oauth/token` with the stored refresh token and
138+ Claude Code's client id, spread over roughly six minutes, every one of them:
139+
140+ ```
141+ HTTP 429 {"error":{"type":"rate_limit_error","message":"Rate limited. Please try again later."}}
142+ ```
143+
144+ No exchange ever happened, so the refresh token was not consumed and nothing
145+ was damaged. A rejection that persists uniformly across six minutes is not
146+ throttling. And we have already seen this exact pattern once: the Messages API
147+ answers a request shape it declines with `429 rate_limit_error` too, rather than
148+ a 401. On these OAuth surfaces a 429 is not reliable evidence of a rate limit,
149+ which is worth knowing before anyone reads one as "wait and retry".
150+
151+ The honest reading is that this request — endpoint, client id, or grant shape —
152+ is not the one this credential is refreshed with. Finding the one that is would
153+ mean guessing at authentication endpoints while holding someone's live refresh
154+ token, which is not a thing to do on a hunch.
155+
156+ **So refresh is not implemented.** The shipped behaviour is the fallback, and it
157+ is a complete feature: use the stored access token, and when it has expired fail
158+ with `your Claude Code login has expired, run claude to refresh it`. Tokens last
159+ around eight hours, so the cost is an occasional re-login — paid by running the
160+ tool that owns the credential, which is the right place to pay it.
161+
162+ If refresh is wanted later, the open question is unchanged and the probe is
163+ unchanged; what is needed first is a refresh route that actually responds.
149164
150165 ## The system prompt
151166