NousResearch/hermes-agent · error · ValueError
Anthropic refresh response was missing access_token
Error message
Anthropic refresh response was missing access_token
What it means
Raised during Anthropic OAuth token refresh when a refresh endpoint returned HTTP success (urlopen did not raise) but the parsed JSON body contained no non-empty access_token. The code treats a 200-without-token as a protocol violation and aborts rather than persisting a garbage credential.
Source
Thrown at agent/anthropic_adapter.py:1175
endpoint,
data=data,
headers={
"Content-Type": content_type,
"User-Agent": _OAUTH_TOKEN_USER_AGENT,
},
method="POST",
)
try:
with urllib.request.urlopen(req, timeout=10) as resp:
result = json.loads(resp.read().decode())
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 itselfView on GitHub (pinned to c896c09c42)
Solutions
- Reproduce the POST manually (curl the token endpoint with the same grant) and inspect the actual body
- Disable or fix intercepting proxies for the Anthropic auth domain
- Re-run the OAuth login (claude setup-token flow) to obtain a fresh token pair
- If a custom auth base_url is configured, verify it points at the real token endpoint
Defensive patterns
Strategy: try-catch
Validate before calling
def refresh_response_shape_ok(result: dict) -> bool:
return isinstance(result, dict) and bool(result.get("access_token"))
# after fetching the refresh response:
if not refresh_response_shape_ok(result):
log_body_shape(result) # inspect what the endpoint actually returned
reauthenticate() Try / catch
try:
creds = refresh_anthropic_token(refresh_token)
except ValueError as e:
if "missing access_token" in str(e):
reauthenticate() # token endpoint is compromised/intercepted — re-login
else:
raise Prevention
- Bypass intercepting proxies for Anthropic auth domains
- Validate custom auth base URLs against the documented token endpoint before relying on them
- Log (never store) the response shape when refresh behaves unexpectedly, to distinguish MITM from API drift
When it happens
Trigger: POSTing the refresh grant to a candidate endpoint that answers 200 with a body lacking access_token — a corporate proxy/MITM returning a 200 login page, a wrong custom auth base_url hitting a server that wraps errors in 200, or an HTML error page parsed as JSON.
Common situations: Intercepting proxy on the auth domain; custom base_url for the token endpoint pointing at the wrong server; auth infrastructure (e.g. identity provider) changing its response shape.
Related errors
- Anthropic token refresh failed
- 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/b1380086c3d6b697.
Report an issue: GitHub.