stablyai/orca · error · CredentialDecryptionError
Could not decrypt saved ${service} credential. Approve Keych
Error message
Could not decrypt saved ${service} credential. Approve Keychain access or reconnect ${service}. What it means
Thrown as CredentialDecryptionError when the stored credential file holds bytes that are neither decryptable by Electron safeStorage (or safeStorage is unavailable) nor valid legacy plaintext. Specifically, readPlaintextLegacyCredential's fatal UTF-8 decode fails (decodeUtf8 returns null) OR the decoded string contains control characters — a heuristic that catches safeStorage ciphertext (e.g. macOS v10 blobs) misclassified as plaintext, preventing auth-header corruption. The message names the service and tells the user to approve Keychain access or reconnect.
Source
Thrown at src/main/integration-credential-file.ts:133
try {
return usableToken(safeStorage.decryptString(raw))
} catch {
return readPlaintextLegacyCredential(service, raw)
}
}
return readPlaintextLegacyCredential(service, raw)
}
function readPlaintextLegacyCredential(
service: IntegrationCredentialService,
raw: Buffer
): string | null {
const plaintext = decodeUtf8(raw)
// Why: legacy plaintext tokens are printable UTF-8; safeStorage ciphertext
// such as macOS v10 blobs must not be decoded into auth-header junk.
if (plaintext === null || hasControlCharacter(plaintext)) {
throw new CredentialDecryptionError(service)
}
return usableToken(plaintext)
}
function usableToken(token: string): string | null {
return token.length > 0 ? token : null
}
function decodeUtf8(raw: Buffer): string | null {
try {
return new TextDecoder('utf-8', { fatal: true }).decode(raw)
} catch {
return null
}
}
function hasControlCharacter(value: string): boolean {
for (let index = 0; index < value.length; index += 1) {View on GitHub (pinned to 1136503c6a)
Solutions
- Reconnect the integration through the UI to overwrite the credential with a freshly-encrypted token.
- On macOS, approve the Keychain access prompt (or fix the app's keychain entry after re-signing).
- On Linux, ensure a secrets service (gnome-keyring/kwallet/libsecret) is running so safeStorage works.
- As a last resort, delete the corrupt credential file so a new one is written on next connect.
Defensive patterns
Strategy: try-catch
Validate before calling
import { safeStorage } from 'electron'
function canDecryptCredential(): boolean {
return safeStorage.isEncryptionAvailable()
} Type guard
function isCredentialDecryptionError(error: unknown): error is CredentialDecryptionError {
return error instanceof Error && error.name === 'CredentialDecryptionError'
} Try / catch
try {
token = readStoredCredentialToken(service, raw)
} catch (error) {
if (error instanceof CredentialDecryptionError || (error instanceof Error && error.name === 'CredentialDecryptionError')) {
// prompt user to reconnect the integration to overwrite the credential
await promptReconnect(service)
return null
}
throw error
} Prevention
- Ensure a secrets service (gnome-keyring/kwallet/libsecret) is running on Linux before storing credentials.
- After app re-signing/re-notarization on macOS, expect to re-auth integrations.
- Never copy credential files between machines/users — they are key-bound.
When it happens
Trigger: safeStorage.decryptString threw (or safeStorage unavailable) so the legacy plaintext path is tried, but the bytes are actually ciphertext: they fail fatal UTF-8 decoding, or decode to a string with control characters. Classic after an app re-sign invalidated the keychain key, or the OS keychain access was denied.
Common situations: macOS app re-signing/re-notarization invalidated the safeStorage key; user denied the Keychain access prompt; credential file copied between machines/users (different safeStorage keys); headless Linux without libsecret/gnome-keyring (safeStorage unavailable) holding ciphertext from a previous GUI session; OS migration.
Related errors
- pending host credential cleanup storage unreadable
- Bitbucket request failed: no usable credential
- Could not read macOS Keychain item ${service}/${account}.
- Cannot capture current Claude Keychain credentials
- Claude login completed, but no OAuth credentials were captur
AI-assisted analysis of stablyai/orca@1136503c6a (2026-08-12).
Data as JSON: /api/errors/d823de6ecc8e3da4.
Report an issue: GitHub.