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

  1. Call await adapter.isAvailable() first; when false use SessionOnlyKeychainAdapter — the documented, deliberate fallback
  2. Install the binding: npm install @napi-rs/keyring; avoid --omit=optional in production images
  3. On headless Linux, also ensure a Secret Service backend (gnome-keyring + D-Bus) so the canary round-trip succeeds
  4. 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

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


AI-assisted analysis of ruvnet/ruflo@fa13ee4ad6 (2026-08-18). Data as JSON: /api/errors/04d01def57d5ed95. Report an issue: GitHub.