{"record":{"id":"604d368a9876b3c9","repo":"NousResearch/hermes-agent","slug":"secure-token-storage-returned-no-encrypted-payload","errorCode":null,"errorMessage":"Secure token storage returned no encrypted payload; refusing to overwrite stored native tokens.","messagePattern":"Secure token storage returned no encrypted payload; refusing to overwrite stored native tokens\\.","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"apps/desktop/electron/native-token-store.ts","lineNumber":113,"sourceCode":" * Write (or, with `tokens === null`, drop) one gateway's token set, merging\n * into whatever other gateways are already stored.\n */\nexport function persistNativeTokenSet(baseUrl: string, tokens: NativeTokenSet | null, io: NativeTokenStoreIo): void {\n  const store = readStore(io)\n\n  if (tokens) {\n    // Encrypt the whole set as one blob so the refresh token never lands in\n    // plaintext on disk. Deliberately outside the try below: an unusable\n    // keychain is an authoritative write failure and must surface to the\n    // caller, not be logged away as if the tokens were saved.\n    const secret = io.encrypt(JSON.stringify(tokens))\n\n    if (!secret) {\n      // A null blob is the same failure as a throw, only quieter. Storing it\n      // would replace a good entry with nothing: the write would report\n      // success, the next launch would show signed out, and the refresh token\n      // would be unrecoverable. Fail before touching the store.\n      throw new Error('Secure token storage returned no encrypted payload; refusing to overwrite stored native tokens.')\n    }\n\n    store[baseUrl] = secret\n  } else {\n    delete store[baseUrl]\n  }\n\n  try {\n    io.writeStoreText(JSON.stringify(store))\n  } catch (error) {\n    const detail = error instanceof Error ? error.message : String(error)\n\n    io.rememberLog?.(`[native-oauth] failed to persist tokens: ${detail}`)\n  }\n}\n\n/**\n * Reconstruct a gateway's token set from the stored encrypted payload. Returns","sourceCodeStart":95,"sourceCodeEnd":131,"githubUrl":"https://github.com/NousResearch/hermes-agent/blob/c896c09c42910c584c4c7d2325b58c14713ea42c/apps/desktop/electron/native-token-store.ts#L95-L131","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Ensure a secret service is available: install and start gnome-keyring (or keepassxc with libsecret integration) so Electron safeStorage can encrypt.","Inspect io.encrypt's failure paths — if it swallows the underlying error, log safeStorage.isEncryptionAvailable() to identify the root cause.","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.","Probe encryption availability at startup and disable persistent token storage (memory-only sessions) when safeStorage is absent."],"exampleFix":"// before\nconst secret = io.encrypt(JSON.stringify(tokens))\nif (!secret) throw new Error('Secure token storage returned no encrypted payload; refusing to overwrite stored native tokens.')\n\n// after (diagnose availability before attempting the write)\nif (!io.isEncryptionAvailable?.()) {\n  log.warn('safeStorage unavailable; keeping native tokens in memory only')\n  return\n}\nconst secret = io.encrypt(JSON.stringify(tokens))\nif (!secret) throw new Error('Secure token storage returned no encrypted payload; refusing to overwrite stored native tokens.')","handlingStrategy":"try-catch","validationCode":"// Check encryption availability before offering persistent native OAuth\nif (typeof io.isEncryptionAvailable === 'function' && !io.isEncryptionAvailable()) {\n  // keep tokens in memory only; skip the persistent write path\n  skipPersistentStore = true\n}","typeGuard":null,"tryCatchPattern":"try {\n  saveNativeTokens(baseUrl, tokens)\n} catch (e) {\n  if (e instanceof Error && e.message.includes('no encrypted payload')) {\n    // keychain unavailable: surface a clear 'cannot persist sign-in' notice,\n    // keep the session in memory; never retry in a loop and never let a null blob overwrite the store\n    notifyUser('Secure storage unavailable; you will be signed out on restart.')\n  } else {\n    throw e\n  }\n}","preventionTips":["Probe Electron safeStorage availability at startup and disable token persistence when absent.","On Linux, ensure a secret service (gnome-keyring / keepassxc) is running before the first sign-in.","Keep the in-memory token set alive so an unavailable keychain degrades to 'session-only', not a crash."],"tags":["security","keychain","electron","token-storage"],"backgroundTag":null,"analyzedSha":"c896c09c42910c584c4c7d2325b58c14713ea42c","analyzedAt":"2026-08-14T17:18:01.089Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}