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 adopt

View on GitHub (pinned to c896c09c42)

Solutions

  1. Re-authenticate interactively (run the claude setup-token login) to mint a fresh token pair
  2. If Claude Code also uses these credentials, avoid two owners racing the single-use refresh token
  3. Check network/proxy reachability of the token endpoints
  4. 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

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


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