NousResearch/hermes-agent · critical

Session token not available — page must be served by the Her

Error message

Session token not available — page must be served by the Hermes dashboard server

What it means

gatewayClient.connect() needs an auth query parameter for the WebSocket upgrade. It builds one via buildWsAuthParam(): a single-use ticket in gated mode, or the injected session token in loopback mode. If that value is empty, the page was not served by (or initialized by) the Hermes dashboard server, so no credential exists to authenticate the WS handshake.

Source

Thrown at web/src/lib/gatewayClient.ts:50

      closedErrorMessage: "WebSocket closed",
      connectErrorMessage: "WebSocket connection failed",
      notConnectedErrorMessage: "gateway not connected",
      onSocketClose: (event) => maybeReloadForLoopbackWsAuthFailure(event.code),
      requestIdPrefix: "w",
    });
  }

  async connect(token?: string): Promise<void> {
    if (this.connectionState === "open" || this.connectionState === "connecting") {
      return;
    }

    // Gated mode: legacy ``?token=`` is rejected by ``_ws_auth_ok``; the SPA
    // must fetch a single-use ticket. Explicit ``token`` keeps the test-only
    // override path.
    const authParam = token ? (["token", token] as const) : await buildWsAuthParam();
    if (!authParam[1]) {
      throw new Error(
        "Session token not available — page must be served by the Hermes dashboard server",
      );
    }

    await super.connect(
      buildHermesWebSocketUrl({
        authParam,
        basePath: HERMES_BASE_PATH,
        path: "/api/ws",
      }),
    );
  }
}

View on GitHub (pinned to c896c09c42)

Solutions

  1. Serve the SPA from the Hermes dashboard server itself (`hermes dashboard`) so it injects the session token / sets the auth cookie.
  2. If developing against a separate frontend server, proxy the app through the dashboard backend or pass an explicit token to connect(token) (test-only path).
  3. Ensure connect() is only called after page bootstrap scripts have run (window.__HERMES_SESSION_TOKEN__ is populated).
Defensive patterns

Strategy: validation

Validate before calling

const wsCredential =
  window.__HERMES_AUTH_REQUIRED__ || window.__HERMES_SESSION_TOKEN__
  ? await buildWsAuthParam()
  : null
if (!wsCredential?.[1]) {
  showFatal('Dashboard must be opened via the Hermes dashboard server (hermes dashboard)')
  return
}

Type guard

function hasInjectedCredential(): boolean {
  return typeof window.__HERMES_SESSION_TOKEN__ === 'string'
    && window.__HERMES_SESSION_TOKEN__.length > 0
}

Try / catch

try {
  await client.connect()
} catch (err) {
  if (String(err).includes('Session token not available')) {
    window.location.reload() // re-bootstrap from the dashboard server
    return
  }
  throw err
}

Prevention

When it happens

Trigger: Opening the built SPA from file://, a static host, or a different origin that never injected window.__HERMES_SESSION_TOKEN__ and has no session cookie; also when the injected token is an empty string in loopback mode.

Common situations: Developing the SPA with `vite dev` against a separately served backend without the token bootstrap; copying the web/dist folder to a CDN; or a race where connect() runs before the server-injected bootstrap script executed.

Related errors


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