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

  1. Treat the error as 'signed out': clear the stored entry for that baseUrl and re-run the native OAuth flow to mint fresh tokens.
  2. Audit every writer of the store: gateway responses are snake_case and must be normalized (camelCase) before persisting — parseStoredTokenSet deliberately does not accept them.
  3. If the store file is corrupted, delete it and let the app re-authenticate.
  4. 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

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


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