# NousResearch/hermes-agent · xAI OAuth loopback callback timeout and fallback fixes

## Open now

- Nothing open on GitHub.

Multiple issues and fixes addressing xAI OAuth loopback server failures on macOS, where the local callback is received but the auth flow times out. Changes include diagnostics, threaded callback handling, 'not received' page display, and manual paste fallback on timeout.

## TL;DR

- 8 member(s): 0 open issue(s), 2 closed issue(s), 0 open PR(s), 3 merged PR(s), 3 PR(s) closed without merging.
- A fix was merged to main in PR #28110. Verify it addresses your case.
- **Recommended:** read PR #28110 (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: `partially_resolved` · deciding PR(s): `pr-33231`. 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.23`: 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

- #27385: \[Bug\]: xAI OAuth loopback on macOS: local callback received, but Hermes times out
- #32984: \[Setup\]: Problem connecting to xAI Grok OAuth (Grok Business, includes SuperGrok)

## Prior & competing PRs

- **#28110: fix(xai-oauth): make loopback callback server threaded + latch first result**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`; implication `superseded`
  - files: `hermes_cli/auth.py`, `scripts/release.py`, `tests/hermes_cli/test_auth_xai_oauth_provider.py`
- **#28112: fix(xai-oauth): show 'not received' page when loopback callback has no code**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`; implication `superseded`
  - files: `hermes_cli/auth.py`, `tests/hermes_cli/test_auth_xai_oauth_provider.py`
  - fix hunks:
  - `hermes_cli/auth.py`

    ```diff
    @@ -2405,15 +2405,37 @@ def do_GET(self) -> None:  # noqa: N802
                     "error": params.get("error", [None])[0],
                     "error_description": params.get("error_description", [None])[0],
                 }
    +
    +            # Treat a hit on the callback path with neither `code` nor `error`
    +            # as a missing OAuth callback (e.g. xAI's auth backend failed to
    +            # redirect and the user navigated to the bare loopback URL by hand).
    +            # Show an explicit "not received" page rather than the success page —
    +            # otherwise the browser claims authorization succeeded while the CLI
    +            # is still waiting for a real callback and eventually times out.
    +            if incoming["code"] is None and incoming["error"] is None:
    +                self.send_response(400)
    +                self._maybe_write_cors_headers()
    +                self.send_header("Content-Type", "text/html; charset=utf-8")
    +                self.end_headers()
    +                body = (
    +                    "<html><body>"
    +                    "<h1>xAI authorization not received.</h1>"
    +                    "<p>No authorization code was present in this callback URL. "
    +                    "Return to the terminal and re-run "
    +                    "<code>hermes auth add xai-oauth</code> to retry.</p>"
    +                    "</body></html>"
    +                )
    +                self.wfile.write(body.encode("utf-8"))
    +                return
    +
                 # ThreadingHTTPServer allows a fallback/manual callback to complete
                 # while a browser connection is stuck.  Once we have a terminal
                 # OAuth result (code or error), keep the first one so a later
                 # concurrent/invalid callback cannot overwrite state before
                 # validation in _xai_oauth_loopback_login().
    -            if incoming["code"] or incoming["error"]:
    -                with result_lock:
    -                    if not (result["code"] or result["error"]):
    -                        result.update(incoming)
    +            with result_lock:
    +                if not (result["code"] or result["error"]):
    +                    result.update(incoming)
     
                 self.send_response(200)
                 self._maybe_write_cors_headers()
    ```
  - `tests/hermes_cli/test_auth_xai_oauth_provider.py`

    ```diff
    @@ -326,6 +326,84 @@ def test_xai_callback_server_latches_first_terminal_callback_result():
             thread.join(timeout=1.0)
     
     
    +# ---------------------------------------------------------------------------
    +# Loopback callback handler GET responses
    +# ---------------------------------------------------------------------------
    +
    +
    +def _get_callback(redirect_uri: str, query: str = "") -> tuple[int, str]:
    +    """GET the loopback callback URL with an optional query string."""
    +    from urllib.request import Request, urlopen
    +    from urllib.error import HTTPError
    +
    +    target = redirect_uri + (("?" + query) if query else "")
    +    req = Request(target, method="GET")
    +    try:
    +        with urlopen(req, timeout=5.0) as resp:
    +            return resp.getcode(), resp.read().decode("utf-8", "replace")
    +    except HTTPError as exc:
    +        return exc.code, exc.read().decode("utf-8", "replace")
    +
    +
    +def test_xai_callback_handler_returns_400_when_callback_url_lacks_code_and_error():
    +    """Bare loopback URL (no code, no error) must not claim authorization received.
    +
    +    Regression for #27385: when xAI's auth backend fails to redirect and the user
    +    manually navigates to http://127.0.0.1:<port>/callback, the handler used to
    +    return 200 "xAI authorization received" while the CLI's wait loop still timed
    +    out — leaving the user with a contradictory success page and a CLI error.
    +    """
    +    server, thread, result, redirect_uri = _xai_start_callback_server(preferred_port=0)
    +    try:
    +        status, body = _get_callback(redirect_uri)
    +        assert status == 400
    +        assert "not received" in body.lower()
    +        assert "hermes auth add xai-oauth" in body
    +        # Wait loop must still see no code/error so it raises a real timeout,
    +        # rather than treating this empty hit as a successful callback.
    +        assert result["code"] is None
    +        assert result["error"] is None
    +    finally:
    +        server.shutdown()
    +        server.server_close()
    +        thread.join(timeout=1.0)
    +
    +
    +def test_xai_callback_handler_accepts_callback_with_code():
    +    """A real OAuth redirect (code + state) still records both and shows success."""
    +    server, thread, result, redirect_uri = _xai_start_callback_server(preferred_port=0)
    +    try:
    +        status, body = _get_callback(redirect_uri, query="code=abc&state=xyz")
    +        assert status == 200
    +        assert "xAI authorization received" in body
    +        assert result["code"] == "abc"
    +        assert result["state"] == "xyz"
    +        assert result["error"] is None
    +    finally:
    +        server.shutdown()
    +        server.server_close()
    +        thread.join(timeout=1.0)
    +
    +
    +def test_xai_callback_handler_records_error_callback():
    +    """A redirect carrying an `error` param must surface the failure page and capture detail."""
    +    server, thread, result, redirect_uri = _xai_start_callback_server(preferred_port=0)
    +    try:
    +        status, body = _get_callback(
    +            redirect_uri,
    +            query="error=access_denied&error_description=user%20cancelled",
    +        )
    +        assert status == 200
    +        assert "xAI authorization failed" in body
    +        assert result["error"] == "access_denied"
    +        assert result["error_description"] == "user cancelled"
    +        assert result["code"] is None
    +    finally:
    +        server.shutdown()
    +        server.server_close()
    +        thread.join(timeout=1.0)
    +
    +
     # ---------------------------------------------------------------------------
     # Token roundtrip + reads
     # ---------------------------------------------------------------------------
    ```
- **#33231: fix(xai-oauth): fall back to manual paste on loopback timeout (#33006)**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`; implication `superseded`
  - files: `hermes_cli/auth.py`, `tests/hermes_cli/test_auth_manual_paste.py`
- **#27439: fix(xai-oauth): show "not received" page when loopback callback has no code (#27385)**. closed without merge
  - maintainer direction: category `merged_via`; implication `superseded`; refs `#28112`, `#28110`
- **#33006: Fix xAI OAuth timeout manual fallback**. closed without merge
  - maintainer direction: category `merged_via`; implication `superseded`; refs `1b9c247b9`, `#33231`, `#31894`, `#31894`
- **#33289: fix(xai-oauth): fall back to manual paste on loopback timeout (#33006)**. closed without merge
  - maintainer direction: category `duplicate_of`; implication `duplicate`; refs `#33231`

## Fix status

- Merged to main in PR #28110 (cited above). No release data is tracked. Verify downstream.
- Fix last verified (merged) `2026-05-28`.
- Subsystem: `agent`
