{"record":{"id":"b1380086c3d6b697","repo":"NousResearch/hermes-agent","slug":"anthropic-refresh-response-was-missing-access-toke","errorCode":null,"errorMessage":"Anthropic refresh response was missing access_token","messagePattern":"Anthropic refresh response was missing access_token","errorType":"exception","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"agent/anthropic_adapter.py","lineNumber":1175,"sourceCode":"            endpoint,\n            data=data,\n            headers={\n                \"Content-Type\": content_type,\n                \"User-Agent\": _OAUTH_TOKEN_USER_AGENT,\n            },\n            method=\"POST\",\n        )\n        try:\n            with urllib.request.urlopen(req, timeout=10) as resp:\n                result = json.loads(resp.read().decode())\n        except Exception as exc:\n            last_error = exc\n            logger.debug(\"Anthropic token refresh failed at %s: %s\", endpoint, exc)\n            continue\n\n        access_token = result.get(\"access_token\", \"\")\n        if not access_token:\n            raise ValueError(\"Anthropic refresh response was missing access_token\")\n        next_refresh = result.get(\"refresh_token\", refresh_token)\n        expires_in = result.get(\"expires_in\", 3600)\n        return {\n            \"access_token\": access_token,\n            \"refresh_token\": next_refresh,\n            \"expires_at_ms\": int(time.time() * 1000) + (expires_in * 1000),\n        }\n\n    if last_error is not None:\n        raise last_error\n    raise ValueError(\"Anthropic token refresh failed\")\n\n\ndef _refresh_oauth_token(creds: Dict[str, Any]) -> Optional[str]:\n    \"\"\"Attempt to refresh an expired Claude Code OAuth token.\n\n    Claude Code's OAuth refresh tokens are single-use: a successful refresh\n    rotates the pair and invalidates the old refresh token. Claude Code itself","sourceCodeStart":1157,"sourceCodeEnd":1193,"githubUrl":"https://github.com/NousResearch/hermes-agent/blob/c896c09c42910c584c4c7d2325b58c14713ea42c/agent/anthropic_adapter.py#L1157-L1193","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":"def refresh_response_shape_ok(result: dict) -> bool:\n    return isinstance(result, dict) and bool(result.get(\"access_token\"))\n\n# after fetching the refresh response:\nif not refresh_response_shape_ok(result):\n    log_body_shape(result)  # inspect what the endpoint actually returned\n    reauthenticate()","typeGuard":null,"tryCatchPattern":"try:\n    creds = refresh_anthropic_token(refresh_token)\nexcept ValueError as e:\n    if \"missing access_token\" in str(e):\n        reauthenticate()  # token endpoint is compromised/intercepted — re-login\n    else:\n        raise","preventionTips":["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"],"tags":["oauth","anthropic","auth","proxy"],"backgroundTag":null,"analyzedSha":"c896c09c42910c584c4c7d2325b58c14713ea42c","analyzedAt":"2026-08-14T17:18:01.089Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}