NousResearch/hermes-agent · error · Error

Secure token storage returned no encrypted payload; refusing

Error message

Secure token storage returned no encrypted payload; refusing to overwrite stored native tokens.

What it means

Thrown by the native token store writer in the Hermes desktop app when io.encrypt() returns a falsy value instead of an encrypted blob. The whole token set is encrypted as one blob so the refresh token never lands in plaintext, and this check is deliberately placed outside the surrounding try: an unusable keychain is an authoritative write failure that must surface to the caller, not be logged away as if the tokens were saved.

Source

Thrown at apps/desktop/electron/native-token-store.ts:113

 * Write (or, with `tokens === null`, drop) one gateway's token set, merging
 * into whatever other gateways are already stored.
 */
export function persistNativeTokenSet(baseUrl: string, tokens: NativeTokenSet | null, io: NativeTokenStoreIo): void {
  const store = readStore(io)

  if (tokens) {
    // Encrypt the whole set as one blob so the refresh token never lands in
    // plaintext on disk. Deliberately outside the try below: an unusable
    // keychain is an authoritative write failure and must surface to the
    // caller, not be logged away as if the tokens were saved.
    const secret = io.encrypt(JSON.stringify(tokens))

    if (!secret) {
      // A null blob is the same failure as a throw, only quieter. Storing it
      // would replace a good entry with nothing: the write would report
      // success, the next launch would show signed out, and the refresh token
      // would be unrecoverable. Fail before touching the store.
      throw new Error('Secure token storage returned no encrypted payload; refusing to overwrite stored native tokens.')
    }

    store[baseUrl] = secret
  } else {
    delete store[baseUrl]
  }

  try {
    io.writeStoreText(JSON.stringify(store))
  } catch (error) {
    const detail = error instanceof Error ? error.message : String(error)

    io.rememberLog?.(`[native-oauth] failed to persist tokens: ${detail}`)
  }
}

/**
 * Reconstruct a gateway's token set from the stored encrypted payload. Returns

View on GitHub (pinned to c896c09c42)

Solutions

  1. Ensure a secret service is available: install and start gnome-keyring (or keepassxc with libsecret integration) so Electron safeStorage can encrypt.
  2. Inspect io.encrypt's failure paths — if it swallows the underlying error, log safeStorage.isEncryptionAvailable() to identify the root cause.
  3. Do not retry in a loop or catch-and-continue: the comment is explicit that a null blob would overwrite a good entry with nothing (silent sign-out, unrecoverable refresh token). Surface a 'could not securely store credentials' state and keep the session in memory only.
  4. Probe encryption availability at startup and disable persistent token storage (memory-only sessions) when safeStorage is absent.

Example fix

// before
const secret = io.encrypt(JSON.stringify(tokens))
if (!secret) throw new Error('Secure token storage returned no encrypted payload; refusing to overwrite stored native tokens.')

// after (diagnose availability before attempting the write)
if (!io.isEncryptionAvailable?.()) {
  log.warn('safeStorage unavailable; keeping native tokens in memory only')
  return
}
const secret = io.encrypt(JSON.stringify(tokens))
if (!secret) throw new Error('Secure token storage returned no encrypted payload; refusing to overwrite stored native tokens.')
Defensive patterns

Strategy: try-catch

Validate before calling

// Check encryption availability before offering persistent native OAuth
if (typeof io.isEncryptionAvailable === 'function' && !io.isEncryptionAvailable()) {
  // keep tokens in memory only; skip the persistent write path
  skipPersistentStore = true
}

Try / catch

try {
  saveNativeTokens(baseUrl, tokens)
} catch (e) {
  if (e instanceof Error && e.message.includes('no encrypted payload')) {
    // keychain unavailable: surface a clear 'cannot persist sign-in' notice,
    // keep the session in memory; never retry in a loop and never let a null blob overwrite the store
    notifyUser('Secure storage unavailable; you will be signed out on restart.')
  } else {
    throw e
  }
}

Prevention

When it happens

Trigger: Saving native tokens (tokens truthy) when io.encrypt(JSON.stringify(tokens)) returns null/undefined/empty. Typical causes: the OS keychain is unavailable (Linux without a secret service, headless session, locked keyring), Electron safeStorage failed to initialize, or the encrypt helper returns null on any internal failure.

Common situations: Linux desktop without gnome-keyring/libsecret running; running the Electron app over a bare SSH X-forward session with no keyring daemon; keyring locked at sign-in time; Electron safeStorage unavailable on the platform.

Related errors


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