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
- Serve the SPA from the Hermes dashboard server itself (`hermes dashboard`) so it injects the session token / sets the auth cookie.
- 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).
- 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
- Always serve the SPA from the dashboard backend so credentials are injected.
- Delay connect() until bootstrap globals are present.
- In dev, proxy through the backend or pass an explicit test token.
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
- Gateway did not return a WS ticket.
- Hermes backend for profile "${profile}" is HTTP-reachable bu
- Local Hermes backend is HTTP-reachable but the WebSocket (/a
- /api/auth/ws-ticket: HTTP ${res.status}
- Hermes install at ${ACTIVE_HERMES_ROOT} is missing or incomp
AI-assisted analysis of NousResearch/hermes-agent@c896c09c42 (2026-08-14).
Data as JSON: /api/errors/8da1cf1b2fa62915.
Report an issue: GitHub.