# NousResearch/hermes-agent · OAuth token refresh endpoint and reliability fixes

## Open now

- Nothing open on GitHub.

Multiple issues and fixes addressing OAuth token refresh failures, including wrong endpoints causing 401/403 errors, migration to platform.claude.com, atomic credential writes, stale detection, and removal of an unused PKCE flow.

## TL;DR

- 4 member(s): 0 open issue(s), 2 closed issue(s), 0 open PR(s), 1 merged PR(s), 1 PR(s) closed without merging.
- A fix was merged to main in PR #3246. Verify it addresses your case.
- **Recommended:** read PR #3246 (and any competing PRs below) before filing. Confirm the merged change covers your symptom.

## Maintainer resolution verdict

- Internal-tracker verdict over this cluster's issue members: `unresolved`. This is distinct from the fix status above. Weigh it yourself.

## How this cluster was grouped

- Link evidence (`STRUCTURAL_PARTIAL`): some members are connected by structural evidence (shared changed files, or a closing/superseded reference); the rest are joined by an automated relevance judgement over their text.
- Members outside that structural core may belong to a different underlying problem.
- Pairwise link density `0.39`: the share of member pairs that carry a link, counting a structural link in full and a relevance-only link at 0.36. `1.00` means every pair is linked.

## Closed / duplicate issues

- #2962: \[Bug\]: OAuth token refresh fails on headless gateway — wrong endpoint + no recovery, causing persistent 401s with Anthropic Max
- #6347: Anthropic OAuth refresh path gets Cloudflare 403 and leaves Hermes PKCE creds unrefreshable

## Prior & competing PRs

- **#3246: fix(auth): migrate OAuth token refresh to platform.claude.com with fallback**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`; implication `superseded`
  - files: `agent/anthropic_adapter.py`
  - fix hunks:
  - `agent/anthropic_adapter.py`

    ```diff
    @@ -210,9 +210,12 @@ def _refresh_oauth_token(creds: Dict[str, Any]) -> Optional[str]:
         Only works for credentials that have a refresh token (from claude /login
         or claude setup-token with OAuth flow).
     
    +    Tries the new platform.claude.com endpoint first (Claude Code >=2.1.81),
    +    then falls back to console.anthropic.com for older tokens.
    +
         Returns the new access token, or None if refresh fails.
         """
    -    import urllib.parse
    +    import time
         import urllib.request
     
         refresh_token = [redacted:secret-assignment]"refreshToken", "")
    ```

    ```diff
    @@ -223,38 +226,42 @@ def _refresh_oauth_token(creds: Dict[str, Any]) -> Optional[str]:
         # Client ID used by Claude Code's OAuth flow
         CLIENT_ID = "9d1c250a-e61b-44d9-88ed-5944d1962f5e"
     
    -    data = urllib.parse.urlencode({
    +    # Anthropic migrated OAuth from console.anthropic.com to platform.claude.com
    +    # (Claude Code v2.1.81+). Try new endpoint first, fall back to old.
    +    token_endpoints = [
    +        "https://platform.claude.com/v1/oauth/token",
    +        "https://console.anthropic.com/v1/oauth/token",
    +    ]
    +
    +    payload = json.dumps({
             "grant_type": "refresh_token",
             "refresh_token": refresh_token,
             "client_id": CLIENT_ID,
         }).encode()
     
    -    req = urllib.request.Request(
    -        "https://console.anthropic.com/v1/oauth/token",
    -        data=data,
    -        headers={
    -            "Content-Type": "application/x-www-form-urlencoded",
    -            "User-Agent": f"claude-cli/{_CLAUDE_CODE_VERSION} (external, cli)",
    -        },
    -        method="POST",
    -    )
    +    headers = {
    +        "Content-Type": "application/json",
    +        "User-Agent": f"claude-cli/{_CLAUDE_CODE_VERSION} (external, cli)",
    +    }
     
    -    try:
    -        with urllib.request.urlopen(req, timeout=10) as resp:
    -            result = json.loads(resp.read().decode())
    -            new_access = result.get("access_token", "")
    -            new_refresh = result.get("refresh_token", refresh_token)
    -            expires_in = result.get("expires_in", 3600)  # seconds
    -
    -            if new_access:
    -                import time
    -                new_expires_ms = int(time.time() * 1000) + (expires_in * 1000)
    -                # Write refreshed credentials back to ~/.claude/.credentials.json
    -                _write_claude_code_credentials(new_access, new_refresh, new_expires_ms)
    -                logger.debug("Successfully refreshed Claude Code OAuth token")
    -                return new_access
    -    except Exception as e:
    -        logger.debug("Failed to refresh Claude Code token: %s", e)
    +    for endpoint in token_endpoints:
    +        req = urllib.request.Request(
    +            endpoint, data=payload, headers=headers, method="POST",
    +        )
    +        try:
    +            with urllib.request.urlopen(req, timeout=10) as resp:
    +                result = json.loads(resp.read().decode())
    +                new_access = result.get("access_token", "")
    +                new_refresh = result.get("refresh_token", refresh_token)
    +                expires_in = result.get("expires_in", 3600)
    +
    +                if new_access:
    +                    new_expires_ms = int(time.time() * 1000) + (expires_in * 1000)
    +                    _write_claude_code_credentials(new_access, new_refresh, new_expires_ms)
    +                    logger.debug("Refreshed Claude Code OAuth token via %s", endpoint)
    +                    return new_access
    +        except Exception as e:
    +            logger.debug("Token refresh failed at %s: %s", endpoint, e)
     
         return None
     
    ```
- **#2968: fix(oauth): use platform.claude.com endpoint for token refresh + startup warning**. closed without merge
  - maintainer direction: category `superseded_by_pr`; implication `superseded`; refs `#3246`

## Fix status

- Merged to main in PR #3246 (cited above). No release data is tracked. Verify downstream.
- Fix last verified (merged) `2026-03-26`. This confirmation is old and may be out of date (a revert/reopen would not have invalidated it here); re-verify.
- Subsystem: `agent`
