JuliusBrussee/caveman · error · Error

could not remove credentials from macOS Keychain

Error message

could not remove credentials from macOS Keychain

What it means

Thrown when deleting stored credentials from the macOS Keychain via `security delete-generic-password` fails with an exit status other than 44 (errSecItemNotFound, which is treated as success since the goal is removal). This means the credential entry may still exist in the Keychain after a logout/cleanup. It is a defensive guard so silent partial logouts are detected.

Solutions

  1. Unlock the Keychain (open Keychain Access, or `security unlock-keychain`) and retry the logout.
  2. Re-run the logout command and click 'Allow' on any Keychain access prompt.
  3. Check that /usr/bin/security exists and is functional (`security --version`).
  4. Remove the entry manually: `security delete-generic-password -s <service> -a <account>` and inspect the reported error.

Example fix

// before
execFileSync("security", ["delete-generic-password", "-s", service, "-a", account], { stdio: "ignore" });
// after
// unlock first, then retry:
execFileSync("security", ["unlock-keychain"]); // or open Keychain Access and unlock
execFileSync("security", ["delete-generic-password", "-s", service, "-a", account], { stdio: "ignore" });
Defensive patterns

Strategy: try-catch

Validate before calling

// pre-check the item exists before removal
const status = require("child_process").spawnSync(
  "security", ["find-generic-password", "-s", service, "-a", account], { stdio: "ignore" });
if (status.status === 0) {
  // item exists; ensure Keychain is unlocked before delete
  require("child_process").execFileSync("security", ["unlock-keychain"]);
}

Type guard

function isKeychainDeleteFailure(e: unknown): e is Error {
  return e instanceof Error && e.message.includes("could not remove credentials from macOS Keychain");
}

Try / catch

try {
  cavemanLogout();
} catch (e) {
  if (isKeychainDeleteFailure(e)) {
    console.error("Keychain delete failed; unlock Keychain Access and retry logout:", e.message);
  } else throw e;
}

Prevention

When it happens

Trigger: execFileSync('security', ['delete-generic-password','-s',service,'-a',account]) exits with any status except 0 or 44: e.g. the Keychain is locked, the security binary is missing/broken, or permission to the item is denied (user clicks Deny).

Common situations: macOS Keychain locked after sleep/restart, TCC/security prompts denied during logout, running in a context without a user Keychain (CI, SSH session without access), or corrupted Keychain entry.

Related errors


AI-assisted analysis of JuliusBrussee/caveman@3ee70a1026 (2026-09-20). Data as JSON: /api/errors/37e8c3d1b86436a5. Report an issue: GitHub.

Appendix: source

Thrown at packages/cli/src/index.ts:18708

function genericKeychainGet(service: string, account: string): string {
  try {
    return execFileSync("security", ["find-generic-password", "-s", service, "-a", account, "-w"], {
      encoding: "utf8",
      stdio: ["ignore", "pipe", "ignore"],
    }).trim();
  } catch {
    return "";
  }
}

function genericKeychainDelete(service: string, account: string) {
  try {
    execFileSync("security", ["delete-generic-password", "-s", service, "-a", account], { stdio: "ignore" });
  } catch (error) {
    // macOS security exits 44 for errSecItemNotFound. Absence is the desired
    // postcondition; every other failure means the secret may still exist.
    if ((error as { status?: unknown }).status === 44) return;
    throw new Error("could not remove credentials from macOS Keychain");
  }
}

function caveHome() {
  return process.env.CAVEMAN_HOME ?? join(homedir(), ".caveman");
}

function credentialsPath() {
  return join(caveHome(), "credentials");
}

function fileTokenSet(token: string) {
  ensureCavemanHome();
  try { chmodSync(credentialsPath(), 0o600); } catch { /* created below */ }
  writeFileSync(credentialsPath(), token, { mode: 0o600 });
  chmodSync(credentialsPath(), 0o600);
}

View on GitHub (pinned to 3ee70a1026)