ruvnet/ruflo · warning · Error
native keychain backend unavailable
Error message
native keychain backend unavailable
What it means
NativeKeychainAdapter.setSecret throws this plain Error when resolveEntryCtor() returns null — the dynamic import('@napi-rs/keyring') failed because the package isn't installed or has no prebuilt binary for the platform. ADR-306 defines the intended degrade path: check isAvailable() and fall back to SessionOnlyKeychainAdapter (memory-only, never persisted) rather than writing secrets to disk unencrypted.
Source
Thrown at v3/@claude-flow/security/src/keychain-adapter.ts:101
}
async isAvailable(): Promise<boolean> {
const Entry = await this.resolveEntryCtor();
if (!Entry) return false;
try {
const entry = new Entry(CANARY_SERVICE, CANARY_ACCOUNT);
entry.setPassword('canary');
const readBack = entry.getPassword();
entry.deletePassword();
return readBack === 'canary';
} catch {
return false; // binding loaded, but no reachable backend (e.g. headless Linux, no D-Bus)
}
}
async setSecret(service: string, account: string, secret: string): Promise<void> {
const Entry = await this.resolveEntryCtor();
if (!Entry) throw new Error('native keychain backend unavailable');
new Entry(service, account).setPassword(secret);
}
async getSecret(service: string, account: string): Promise<string | null> {
const Entry = await this.resolveEntryCtor();
if (!Entry) return null;
try {
return new Entry(service, account).getPassword();
} catch {
return null; // no matching entry, or backend unavailable
}
}
async deleteSecret(service: string, account: string): Promise<void> {
const Entry = await this.resolveEntryCtor();
if (!Entry) return;
try {
new Entry(service, account).deletePassword();View on GitHub (pinned to fa13ee4ad6)
Solutions
- Call await adapter.isAvailable() first; when false use SessionOnlyKeychainAdapter — the documented, deliberate fallback
- Install the binding: npm install @napi-rs/keyring; avoid --omit=optional in production images
- On headless Linux, also ensure a Secret Service backend (gnome-keyring + D-Bus) so the canary round-trip succeeds
- For bundlers, mark @napi-rs/keyring as external so the dynamic import survives packaging
Example fix
// before
await nativeAdapter.setSecret('ruflo', 'refresh-token', token); // throws: native keychain backend unavailable
// after
const adapter = (await nativeAdapter.isAvailable())
? nativeAdapter
: new SessionOnlyKeychainAdapter(); // ADR-306 degrade path: memory-only, never persisted
await adapter.setSecret('ruflo', 'refresh-token', token); Defensive patterns
Strategy: fallback
Validate before calling
import { NativeKeychainAdapter, SessionOnlyKeychainAdapter, KeychainAdapter } from './keychain-adapter.js';
const native = new NativeKeychainAdapter();
const adapter: KeychainAdapter = (await native.isAvailable())
? native
: new SessionOnlyKeychainAdapter(); // memory-only — ADR-306 degrade path
await adapter.setSecret(service, account, secret); Type guard
function isKeychainUnavailable(e: unknown): boolean {
return e instanceof Error && e.message === 'native keychain backend unavailable';
} Try / catch
try {
await nativeAdapter.setSecret(service, account, secret);
} catch (e) {
if (isKeychainUnavailable(e)) {
const session = new SessionOnlyKeychainAdapter();
await session.setSecret(service, account, secret); // warn: lost on exit, never persisted
} else {
throw e;
}
} Prevention
- Always gate native keychain use behind isAvailable() — it runs a real write/read/delete canary
- Never write refresh tokens to disk as a 'workaround'; use the session-only adapter
- Install @napi-rs/keyring as a real dependency in production images; mark it external when bundling
When it happens
Trigger: Calling setSecret after an install skipped optional dependencies (npm --omit=optional, yarn selective resolution); a bundler statically pruning the dynamic import; unsupported platform/arch with no prebuilt napi binary.
Common situations: Slim CI containers and Docker images without optional deps; Alpine/musl or exotic architectures lacking prebuilts; esbuild/webpack builds marking the dynamic import dead code; headless Linux where the module loads but no D-Bus Secret Service exists (canary path).
Related errors
- Dependencies not available
- Pinata JWT required (config.pinataJwt or PINATA_API_JWT)
- profile "${profile}" has no persisted refresh token and its
- ruflo auth needs the '@claude-flow/security' package, which
- "@agntcy/slim-bindings" is installed but does not export pub
AI-assisted analysis of ruvnet/ruflo@fa13ee4ad6 (2026-08-18).
Data as JSON: /api/errors/04d01def57d5ed95.
Report an issue: GitHub.