NousResearch/hermes-agent · error · Error
Stored token set missing accessToken
Error message
Stored token set missing accessToken
What it means
Thrown by parseStoredTokenSet() when validating a token set loaded from the encrypted local store in the Hermes desktop app. The stored representation is normalized as NativeTokenSet (camelCase), and this check enforces a non-empty accessToken before the set is used. A missing accessToken means the persisted credentials are unusable for authenticated gateway calls.
Source
Thrown at apps/desktop/electron/native-oauth.ts:207
refreshToken: String(body?.refresh_token || ''),
expiresAt: Number.isFinite(expiresAt) ? expiresAt : 0,
provider: String(body?.provider || ''),
userId: String(body?.user_id || '')
}
}
/**
* Validate a token set loaded from the encrypted local store.
*
* The stored representation is already normalized as NativeTokenSet and
* therefore uses camelCase. Gateway token responses use snake_case and
* remain handled separately by parseTokenResponse().
*/
export function parseStoredTokenSet(body: any): NativeTokenSet {
const accessToken = String(body?.accessToken || '')
if (!accessToken) {
throw new Error('Stored token set missing accessToken')
}
const expiresAt = Number(body?.expiresAt)
return {
accessToken,
refreshToken: String(body?.refreshToken || ''),
expiresAt: Number.isFinite(expiresAt) ? expiresAt : 0,
provider: String(body?.provider || ''),
userId: String(body?.userId || '')
}
}
/**
* True when a stored token set is at/near expiry and should be refreshed
* before use. `skewSeconds` refreshes slightly early to avoid a race where
* the token expires in flight (mirrors the server's 60s cookie floor).
*/View on GitHub (pinned to c896c09c42)
Solutions
- Treat the error as 'signed out': clear the stored entry for that baseUrl and re-run the native OAuth flow to mint fresh tokens.
- Audit every writer of the store: gateway responses are snake_case and must be normalized (camelCase) before persisting — parseStoredTokenSet deliberately does not accept them.
- If the store file is corrupted, delete it and let the app re-authenticate.
- Add a read-time migration: if body.accessToken is empty but body.access_token exists, convert before calling parseStoredTokenSet.
Example fix
// before
const tokens = parseStoredTokenSet(JSON.parse(io.decrypt(store[baseUrl])))
// after
const raw = JSON.parse(io.decrypt(store[baseUrl]) ?? '{}')
// migrate legacy snake_case payloads before parsing
if (!raw.accessToken && raw.access_token) {
raw.accessToken = raw.access_token
raw.refreshToken = raw.refresh_token
raw.expiresAt = raw.expires_at
}
const tokens = parseStoredTokenSet(raw) Defensive patterns
Strategy: validation
Validate before calling
function hasStoredAccessToken(body: any): boolean {
return Boolean(body && typeof body.accessToken === 'string' && body.accessToken.length > 0)
}
// before parsing
if (!hasStoredAccessToken(raw)) {
// treat as signed out: clear the stale entry and re-authenticate instead of parsing
clearStoredEntry(baseUrl)
return startOAuthFlow()
} Type guard
function isStoredTokenSetLike(body: any): body is { accessToken: string } {
return typeof body?.accessToken === 'string' && body.accessToken.length > 0
} Try / catch
try {
const tokens = parseStoredTokenSet(raw)
} catch (e) {
if (e instanceof Error && e.message === 'Stored token set missing accessToken') {
// recoverable: drop the stale entry, do not surface as a crash
clearStoredEntry(baseUrl)
return null
}
throw e
} Prevention
- Always normalize gateway responses (snake_case, handled by parseTokenResponse) to NativeTokenSet (camelCase) before persisting.
- Delete the store entry on explicit sign-out so parseStoredTokenSet never runs against an empty record.
- Add a store-format version field and migrate old entries on read.
When it happens
Trigger: Calling parseStoredTokenSet(body) where String(body?.accessToken || '') is empty — accessToken undefined, null, or ''. Happens when the store entry was written from a raw snake_case gateway response (access_token) instead of the normalized camelCase set, was partially decrypted/corrupted, or holds an explicitly cleared record.
Common situations: Upgrading the desktop app across a token-store format change; a truncated or corrupted store file after a crash mid-write; a decrypt helper returning an empty object; any writer persisting parseTokenResponse() output directly instead of a NativeTokenSet.
Related errors
- Gateway did not return a WS ticket.
- Remote gateway session token is required.
- Remote Hermes gateway is selected, but no session token is s
- Hermes backend for profile "${profile}" is HTTP-reachable bu
- Local Hermes backend is HTTP-reachable but the WebSocket (/a
AI-assisted analysis of NousResearch/hermes-agent@c896c09c42 (2026-08-14).
Data as JSON: /api/errors/77f61755be243668.
Report an issue: GitHub.