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 itself

View on GitHub (pinned to c896c09c42)

Solutions

  1. Reproduce the POST manually (curl the token endpoint with the same grant) and inspect the actual body
  2. Disable or fix intercepting proxies for the Anthropic auth domain
  3. Re-run the OAuth login (claude setup-token flow) to obtain a fresh token pair
  4. 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

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


AI-assisted analysis of NousResearch/hermes-agent@c896c09c42 (2026-08-14). Data as JSON: /api/errors/b1380086c3d6b697. Report an issue: GitHub.