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
- Unlock the Keychain (open Keychain Access, or `security unlock-keychain`) and retry the logout.
- Re-run the logout command and click 'Allow' on any Keychain access prompt.
- Check that /usr/bin/security exists and is functional (`security --version`).
- 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
- Keep the login Keychain unlocked before running logout in scripts
- Run logout in an interactive session so Keychain permission prompts can be answered
- Verify /usr/bin/security is available (macOS only code path)
- Re-run logout if it fails; absence of the entry is the success condition
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
- cave_redirect_not_allowed
- must be an absolute http(s) URL without credentials
- provider upstream URL must not include userinfo
- ssrf: credentials embedded in URL are forbidden
- ambiguous escaped path sequence
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)