NousResearch/hermes-agent · error · ValueError
Anthropic token refresh failed
Error message
Anthropic token refresh failed
What it means
Terminal failure of the Anthropic OAuth refresh loop: every candidate endpoint raised (last_error is re-raised) or the loop exhausted without success. A root cause documented right below in _refresh_oauth_token: Claude Code refresh tokens are single-use and rotated by Claude Code on its own schedule, so a stale refresh token raced to the server fails with invalid_grant.
Source
Thrown at agent/anthropic_adapter.py:1186
except Exception as exc:
last_error = exc
logger.debug("Anthropic token refresh failed at %s: %s", endpoint, exc)
continue
access_token = result.get("access_token", "")
if not access_token:
raise ValueError("Anthropic refresh response was missing access_token")
next_refresh = result.get("refresh_token", refresh_token)
expires_in = result.get("expires_in", 3600)
return {
"access_token": access_token,
"refresh_token": next_refresh,
"expires_at_ms": int(time.time() * 1000) + (expires_in * 1000),
}
if last_error is not None:
raise last_error
raise ValueError("Anthropic token refresh failed")
def _refresh_oauth_token(creds: Dict[str, Any]) -> Optional[str]:
"""Attempt to refresh an expired Claude Code OAuth token.
Claude Code's OAuth refresh tokens are single-use: a successful refresh
rotates the pair and invalidates the old refresh token. Claude Code itself
also refreshes on its own schedule (IDE/CLI activity), so by the time
Hermes notices an expired token, Claude Code may have already rotated it.
POSTing our now-stale refresh token in that window races Claude Code and
fails with ``invalid_grant``.
So before refreshing, re-read the live credential sources. If Claude Code
has already produced a valid token, adopt it and skip the POST entirely.
Only fall back to refreshing ourselves when no fresh credential is found.
"""
# Claude Code may have already refreshed — adopt its token rather than
# racing it with our (possibly already-rotated) refresh token. Only adoptView on GitHub (pinned to c896c09c42)
Solutions
- Re-authenticate interactively (run the claude setup-token login) to mint a fresh token pair
- If Claude Code also uses these credentials, avoid two owners racing the single-use refresh token
- Check network/proxy reachability of the token endpoints
- Verify system time via NTP — skew breaks token validity
Defensive patterns
Strategy: retry
Validate before calling
def refresh_token_usable(creds: dict, skew_seconds: int = 60) -> bool:
import time
exp = creds.get("expires_at_ms")
if exp is None:
return False
return time.time() * 1000 < exp - skew_seconds * 1000
# only attempt refresh when the access token is actually near expiry
if not refresh_token_usable(creds):
creds = interactive_relogin() # avoid racing the single-use refresh token Try / catch
for attempt in range(2):
try:
token = _refresh_oauth_token(creds)
break
except (ValueError, OSError) as e:
if attempt == 1 or "invalid_grant" not in str(getattr(e, "__cause__", e)):
token = interactive_relogin() # single-use token already rotated — re-login
break Prevention
- Let a single owner (Hermes OR Claude Code CLI) manage the OAuth refresh; never both
- Re-authenticate proactively after long sleep/shutdown periods instead of refreshing stale tokens
- Keep system clocks NTP-synced to avoid spurious invalid_grant
When it happens
Trigger: All refresh endpoints error — network failures, HTTP 4xx (classic invalid_grant from an already-rotated single-use refresh token), or clock skew invalidating the grant — leaving nothing to return.
Common situations: Hermes and the Claude Code CLI sharing the same OAuth credentials and rotating the pair concurrently; machine slept past token expiry; offline or lossy network to the token endpoints; system clock drift.
Related errors
- Anthropic refresh response was missing access_token
- Stored token set missing accessToken
- The 'claude' CLI is not installed. Install it with: npm inst
- Gateway did not return a WS ticket.
- Remote gateway session token is required.
AI-assisted analysis of NousResearch/hermes-agent@c896c09c42 (2026-08-14).
Data as JSON: /api/errors/178bdfdcf39393b4.
Report an issue: GitHub.