Blog

OpenClaw: "your refresh token has already been used" (openai-codex 401)

Your openai-codex agent suddenly stops using Codex and silently falls back to another provider, and the logs show OAuth token refresh failed (401): "Your refresh token has already been used to generate a new access token" (refresh_token_reused). OAuth refresh tokens are single-use — so something keeps handing the gateway a refresh token that's already been spent. On OpenClaw 2026.6.x, the usual reason one instance gets stuck while its siblings on the same version are fine is a split auth store: the runtime writes rotated tokens to a leftover auth-profiles.json while the gateway reads the refresh token from the new SQLite store (openclaw-agent.sqlite), which still holds the consumed one. The fix: run openclaw doctor --fix to collapse the legacy JSON into SQLite, restart the gateway, and — if the token is genuinely reused — re-authenticate.

What you'll see
OAuth token refresh failed (401): "Your refresh token has already been
used to generate a new access token" [refresh_token_reused]
falling back to provider anthropic/claude-sonnet-4-6

# ...while a sibling instance on the SAME OpenClaw version uses Codex fine.

Why the refresh token is "already used"

OpenAI's OAuth refresh tokens are single-use by design: when a client refreshes an access token, the old refresh token is consumed and a new one is issued in its place. Present the consumed one again and the token endpoint returns 401 "refresh token has already been used to generate a new access token." So a recurring 401 isn't an expiry or a network blip — it means OpenClaw is repeatedly handing the endpoint a refresh token that's already been spent. The question is why. Upstream reports collect several ways this happens: a concurrent refresh race (two sessions/agents refresh one profile at once — the first wins, the rest 401), the provider refreshing even when the current access token is still valid, and not persisting the refreshed token to disk so the next cold start reuses the old one (openclaw/openclaw#26322, #62247, #52037).

The usual cause on 2026.6.x: a split auth store

There's one cause those upstream threads don't name, and it's the one that explains the most confusing symptom — only one instance is stuck, on the same version everything else runs fine. In 2026.6.x OpenClaw moved each agent's auth profile store from JSON (auth-profiles.json) to a SQLite store (openclaw-agent.sqlite, table auth_profile_store, row store_key='primary'). If a stray auth-profiles.json is left behind under ~/.openclaw/agents/<agent>/agent/, you get a split store: the runtime keeps writing rotated tokens into the JSON, while the gateway reads the refresh token from SQLite. The two diverge, SQLite holds the already-consumed refresh token, and every refresh 401s — permanently — while the agent quietly falls back to another provider.

The other thing that strands the token is sharing one account: the same ChatGPT account (especially a Team account) signed into Codex in more than one place rotates the single-use refresh token out from under a given pod. A single-login, personal-account instance doesn't diverge — which is again why one box fails and the rest don't.

Confirm it before you touch anything

Don't re-auth blindly — confirm the split first, so you know whether you're fixing the store or the token:

  1. Run openclaw doctor (no --fix yet). On an instance with a leftover JSON it reports the auth-profile SQLite migration as pending and flags unreferenced legacy credential files — that's the split.
  2. Mind the field name. The refresh-token field in the profile JSON is refresh, not refreshToken — a naive grep for "refreshToken" finds nothing and you'll wrongly conclude there's no token. Compare the refresh value in auth-profiles.json against the one stored in auth_profile_store; if they differ in length or hash, the store is split.
  3. Read SQLite the safe way. Open openclaw-agent.sqlite with a python3 script, not inline shell SQL (quoting mangles it). A telltale: the store's updated_at is frozen at the migration-import time — that's expected when every refresh 401s before a new token is ever persisted, so a frozen timestamp is not by itself a broken write path.

Fix it (self-hosted)

Collapse the legacy JSON into SQLite, then restart the gateway:

# remove the split: import legacy JSON -> SQLite, archive the JSON
openclaw doctor --fix

# then restart the gateway so it reloads the reconciled store
openclaw gateway stop && openclaw gateway start

openclaw doctor --fix collapses auth-profiles.json into the SQLite store, archives the JSON (renamed to a .sqlite-import.<ts>.bak sidecar), and reports re-auth required [refresh_token_reused] for each profile whose token is dead. If it prints that line for your openai-codex profile, the split is gone but the token itself is genuinely spent — re-authenticate interactively to mint a fresh one:

openclaw models auth login --provider openai
The trap: don't clear the store and trust --fix to refill it

It's tempting to "start clean" by emptying the SQLite auth_profile_store rows and letting openclaw doctor --fix rebuild them from the JSON. Don't. The importer is nondeterministic under automation — run back-to-back or during a rollout it can skip the import (it only archives the JSON when it actually imports), and a clear followed by a skipped import leaves the agent's auth store empty. The additive doctor --fix (import without clearing) is safe and idempotent; the clear-then-fix sequence has emptied a live store. Reconcile additively, verify the profile landed, and only then bounce the gateway.

Stop it coming back

  • One account, one place. Don't sign the same ChatGPT account (especially Team) into Codex across multiple instances or other tools — each login rotates the single-use refresh token and strands the rest.
  • Take openai-codex out of cron fallback chains if scheduled runs are what keep tripping it. Until the cold-start-refresh and non-persistence bugs are fixed (openclaw/openclaw#57107), serializing refresh per profile and not racing it from many concurrent sessions reduces how often refresh_token_reused fires.
  • After any upgrade, let the auth-store migration finish before re-authing. Re-authenticating through the dashboard mid-rollout can write the new token to JSON while the gateway still reads SQLite — re-creating the split. If that happens, run openclaw doctor --fix again.

Stop babysitting your OpenClaw box

Fix it once — or stop fixing it for good.

Apply the checklist above and keep self-hosting, or skip the maintenance entirely: run your OpenClaw on managed hosting from $6.90/mo, starting with a 7-day free trial. We handle the stale locks, gateway restarts, version upgrades, and uptime — and you can import your existing instance in a couple of minutes. Cancel anytime.

Managed hosting — from $6.90/mo Your own hosted OpenClaw instance with automatic restarts and version upgrades. Starts with a 7-day free trial — import your current setup, keep your channels, cancel anytime.
$199 managed setup — optional Prefer we do it for you? One workspace configured end-to-end: first-run config, one 30-minute onboarding/debug session, and a 7-day follow-up. Limited weekly slots.
  • Managed hosting handles stale .jsonl.lock files, gateway restarts, and version upgrades for you
  • Import your existing OpenClaw setup in minutes — keep your channels and configuration
  • The optional $199 setup is scoped: no custom development, enterprise/SRE support, or unsupported self-hosting repair

If you would rather compare options first, review OpenClaw cloud hosting or see the best OpenClaw hosting options before deciding.

OpenClaw import first screen in OpenClaw Setup dashboard (light theme) OpenClaw import first screen in OpenClaw Setup dashboard (dark theme)
1) Paste import payload
OpenClaw import completed screen in OpenClaw Setup dashboard (light theme) OpenClaw import completed screen in OpenClaw Setup dashboard (dark theme)
2) Review and launch

Related, but not this

How managed hosting avoids this

The split auth store is a side effect of owning the upgrade and the auth-file lifecycle yourself. On managed OpenClaw hosting from Lobsterland, the JSON→SQLite reconcile runs automatically once per version transition, and credentials you add are written directly into the SQLite auth store rather than left in a legacy JSON the gateway ignores — so the divergence that produces refresh_token_reused doesn't form. You add a key in the dashboard; keeping the auth store consistent across upgrades is the platform's job, not yours.

Import your current OpenClaw instance in 1 click

Frequently asked questions

Why does OpenClaw say my refresh token has already been used?

OAuth refresh tokens are single-use: the first refresh consumes the token and returns a new one, so any later attempt to use that same refresh token returns 401 "refresh token has already been used to generate a new access token" (refresh_token_reused). In OpenClaw with openai-codex this means something keeps handing the gateway a refresh token that's already been spent. On 2026.6.x the usual reason a single instance gets stuck is a split auth store — the runtime writes rotated tokens to a leftover auth-profiles.json while the gateway reads the refresh token from the new SQLite store (openclaw-agent.sqlite), which still holds the consumed token.

Why does only one OpenClaw instance get refresh_token_reused when others work?

Because it's per-instance store state, not the OpenClaw binary. If a version-wide bug were to blame, every instance on that version would fail. When one box 401s on token refresh while its siblings on the same version are fine, suspect a split auth store on that one instance (a leftover auth-profiles.json that diverged from openclaw-agent.sqlite) or a refresh token that another login of the same ChatGPT account has rotated out from under that pod.

Does openclaw doctor --fix repair the refresh_token_reused error?

It repairs the split: openclaw doctor --fix collapses the legacy auth-profiles.json into the SQLite store, archives the JSON, and reports re-auth required [refresh_token_reused] for each dead profile. Restart the gateway afterwards. But doctor --fix only removes the divergence — if the refresh token itself is genuinely reused or superseded, you still have to re-authenticate interactively (openclaw models auth login --provider openai). Do not clear or empty the SQLite store first and rely on doctor --fix to repopulate it: its import is nondeterministic under automation and has left a live store empty.

Why does the refresh_token_reused error come back after a gateway restart?

A restart reloads the gateway, but it doesn't reconcile a split auth store or mint a fresh refresh token. If the leftover auth-profiles.json still diverges from openclaw-agent.sqlite, or the same ChatGPT account is still signed into Codex somewhere else and keeps rotating the token, the gateway reads a consumed refresh token again on the next cold start and 401s. Fix the split (doctor --fix), re-authenticate if the token is truly reused, and stop sharing one account across instances.

How do I stop OpenClaw from burning the Codex refresh token repeatedly?

Don't sign the same ChatGPT account (especially a Team account) into Codex in more than one place — each login rotates the single-use refresh token and strands the others. Upstream reports also show the openai-codex provider refreshing even when the access token is still valid and not always persisting the new token, which burns the refresh token on every cold start; until that's fixed, serializing refresh per profile and removing openai-codex from cron fallback chains reduces how often the race fires.

Cookie preferences