hermes - 💡(How to fix) Fix v0.15.0: dashboard SPA reload-loops in loopback mode — /api/auth/me always 401s when auth gate is off

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…

After updating to v0.15.0, running hermes dashboard in loopback / --insecure mode causes the SPA to enter a ~0.5s reload loop. The page briefly renders, then full-page-navigates and reloads repeatedly.

Error Message

@router.get("/api/auth/me", name="auth_me") async def api_auth_me(request: Request): sess = getattr(request.state, "session", None) if sess is None: raise HTTPException(status_code=401, detail="Unauthorized") ...

Root Cause

After updating to v0.15.0, running hermes dashboard in loopback / --insecure mode causes the SPA to enter a ~0.5s reload loop. The page briefly renders, then full-page-navigates and reloads repeatedly.

Fix Action

Fix / Workaround

Tested patch:

Code Example

if (r.status === 401) {
  // ...
  if (!window.__HERMES_AUTH_REQUIRED__) {
    let l = sessionStorage.getItem("hermes.tokenReloadAttempted") === "1";
    if (!l) {
      sessionStorage.setItem("hermes.tokenReloadAttempted", "1");
      return window.location.reload(), new Promise(() => {});
    }
  }
}
if (r.ok) sessionStorage.removeItem("hermes.tokenReloadAttempted");

---

@router.get("/api/auth/me", name="auth_me")
async def api_auth_me(request: Request):
    sess = getattr(request.state, "session", None)
    if sess is None:
        raise HTTPException(status_code=401, detail="Unauthorized")
    ...

---

@router.get("/api/auth/me", name="auth_me")
async def api_auth_me(request: Request):
    """Return the verified session as JSON."""
    if not getattr(request.app.state, "auth_required", False):
        return {
            "user_id": "",
            "email": "",
            "display_name": "local",
            "org_id": "",
            "provider": "loopback",
            "expires_at": 0,
        }
    sess = getattr(request.state, "session", None)
    if sess is None:
        raise HTTPException(status_code=401, detail="Unauthorized")
    return {
        "user_id": sess.user_id,
        "email": sess.email,
        "display_name": sess.display_name,
        "org_id": sess.org_id,
        "provider": sess.provider,
        "expires_at": sess.expires_at,
    }
RAW_BUFFERClick to expand / collapse

Summary

After updating to v0.15.0, running hermes dashboard in loopback / --insecure mode causes the SPA to enter a ~0.5s reload loop. The page briefly renders, then full-page-navigates and reloads repeatedly.

Environment

  • Hermes Agent v0.15.0 (2026.5.28), updated cleanly from v0.14.0
  • Linux host, Python 3.11.15
  • Dashboard launched as hermes dashboard --port 9119 (default loopback bind)
  • Browser on a separate machine, reached via SSH tunnel (ssh -L 9119:127.0.0.1:9119 ...)
  • auth_required is False (no OAuth gate; dashboard.oauth.client_id unset)

Reproduction

  1. Fresh hermes update from any 0.14.x to 0.15.0
  2. hermes dashboard --port 9119 --no-open
  3. Open http://localhost:9119/ in a browser

Observed: page enters a reload loop, ~2 reloads/sec, visible as a flickering chrome.

Diagnosis

The SPA's fetch wrapper (Re() in the built index-*.js) implements a one-shot token-reload on 401:

if (r.status === 401) {
  // ...
  if (!window.__HERMES_AUTH_REQUIRED__) {
    let l = sessionStorage.getItem("hermes.tokenReloadAttempted") === "1";
    if (!l) {
      sessionStorage.setItem("hermes.tokenReloadAttempted", "1");
      return window.location.reload(), new Promise(() => {});
    }
  }
}
if (r.ok) sessionStorage.removeItem("hermes.tokenReloadAttempted");

The intent is to recover stale __HERMES_SESSION_TOKEN__ by full-reloading once. But the SPA's AuthWidget unconditionally calls getAuthMe()/api/auth/me, and the route handler in hermes_cli/dashboard_auth/routes.py always returns 401 when no gated session is attached:

@router.get("/api/auth/me", name="auth_me")
async def api_auth_me(request: Request):
    sess = getattr(request.state, "session", None)
    if sess is None:
        raise HTTPException(status_code=401, detail="Unauthorized")
    ...

In loopback mode the gate is off, so request.state.session is never populated and /api/auth/me always 401s. Meanwhile /api/status 200s and clears hermes.tokenReloadAttempted on success. The two interleave:

  1. /api/status → 200 → clears flag
  2. /api/auth/me → 401 → flag absent → sets flag, window.location.reload()
  3. After reload: /api/status → 200 → clears flag → goto 2

→ infinite reload loop.

Suggested fix

Make /api/auth/me return a Session-shaped placeholder (or a documented "unauthenticated" envelope) when the OAuth gate is disabled. The SPA's AuthWidget then renders harmlessly without tripping the 401 reload path.

Tested patch:

@router.get("/api/auth/me", name="auth_me")
async def api_auth_me(request: Request):
    """Return the verified session as JSON."""
    if not getattr(request.app.state, "auth_required", False):
        return {
            "user_id": "",
            "email": "",
            "display_name": "local",
            "org_id": "",
            "provider": "loopback",
            "expires_at": 0,
        }
    sess = getattr(request.state, "session", None)
    if sess is None:
        raise HTTPException(status_code=401, detail="Unauthorized")
    return {
        "user_id": sess.user_id,
        "email": sess.email,
        "display_name": sess.display_name,
        "org_id": sess.org_id,
        "provider": sess.provider,
        "expires_at": sess.expires_at,
    }

With this applied locally, the dashboard loads cleanly in loopback mode. Happy to open a PR if the placeholder shape is acceptable, or rework as an explicit unauthenticated envelope the SPA recognises.

Related PRs landed in 0.15

  • #33816 / dashboard-auth Phase 6/7 (401 re-auth envelope, AuthWidget)
  • The new reload-on-401 logic in the SPA looks correct; the bug is the server unconditionally 401-ing in loopback mode.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING

hermes - 💡(How to fix) Fix v0.15.0: dashboard SPA reload-loops in loopback mode — /api/auth/me always 401s when auth gate is off