[Bug] Keychain writability probe emits spurious warning when credential already exists (errSecDuplicateItem -25299)

Open 💬 0 comments Opened Jun 22, 2026 by josephsieh

Bug Description

The installation/doctor keychain writability probe emits a false-positive "macOS Keychain is not writable" warning when the user is already logged in, because the probe uses a plain \add-generic-password\ (no \-U\ flag) against the same service/account slot already occupied by the live OAuth credential.

Version: 2.1.185 (build \9d0bb50\, 2026-06-20)
Platform: macOS (Darwin 25.5.0), Apple Silicon
Auth: Claude subscription (OAuth; credential \Claude Code-credentials\/\joseph\ in login.keychain)

Warning produced

\\\
macOS Keychain is not writable (security: SecKeychainItemCreateFromContent (<default>):
The specified item already exists in the keychain.; add-generic-password: returned -25299).
Console login will fail to save your API key.
Run: security unlock-keychain ~/Library/Keychains/login.keychain-db — if that doesn't fix it,
your login keychain password may be out of sync with your account password: open Keychain Access,
select the 'login' keychain, then Edit → Change Password for Keychain 'login'.
\
\\

Why it's wrong

  • \-25299\ is \errSecDuplicateItem\ — the slot is occupied, not locked or permission-denied.
  • \security show-keychain-info login.keychain-db\ → \no-timeout\ (keychain is unlocked).
  • The \Claude Code-credentials\/\joseph\ item existed and was successfully updated (\mdat\ = same day as the warning), proving keychain writes work fine. The real write path uses \security -i\ with \add-generic-password -U -a "…"\ (the \-U\ / update flag), which never collides.
  • The probe omits \-U\, so it issues a pure create into an already-occupied slot → \-25299\ → misreports as "not writable."
  • The keychain unlock/change-password suggestions in the fix message do not apply and would be disruptive.

Root cause (traced in 2.1.185 binary)

The writability probe (\W3e({probeKeychain})\) does:

\\\js
o = await qb("security", ["-i"], { input: r, reject: false, timeout: 5000 });
if (o.exitCode === 0)
return qb("security", ["delete-generic-password", "-a", t, "-s", e], ...), null;
// any non-zero exit → reports "Keychain is not writable"
let s = (o.stderr || o.stdout || "").trim()...;
return { issue: \
macOS Keychain is not writable (\${s})...\, fix: "Run: security unlock-keychain ..." }
\
\\

The probe targets service \e = "Claude Code-credentials"\, account \t = "joseph"\ — the exact same slot the live OAuth credential occupies. The batch input \r\ does a plain \add\ (no \-U\). On a fresh install the slot is empty so the probe works. Once logged in the slot is occupied and every subsequent probe collides.

⚠️ Secondary hazard — delete-on-success uses the real credential slot. The probe's success branch runs \delete-generic-password -a t -s e\ against the same real \Claude Code-credentials\ slot. Today this is harmless only because the plain \add\ fails before reaching the delete. Any fix that makes the probe's \add\ succeed against the live slot (e.g. naively adding \-U\) would cause the success branch to delete the live OAuth credential on every \doctor\/install run. Any fix must avoid letting the probe's \add\ succeed against the real slot.

Repro

  1. Log in via \claude /login\ on macOS.
  2. Trigger the install/doctor flow (observed during the install-time warnings check; also likely reproduced by \claude doctor\ per the same probe path used in #69631).
  3. Observe: "macOS Keychain is not writable … errSecDuplicateItem … returned -25299" — even though auth works fine and the keychain is unlocked.
  4. Confirm: \security find-generic-password -s "Claude Code-credentials"\ → item is there, healthy, \mdat\ is recent.

Suggested fixes

  1. Use a throwaway service name — probe under e.g. \"Claude Code-probe-test"\ (unrelated to any real credential slot), then delete unconditionally on success. Safe regardless of whether the user is logged in, and avoids the delete-on-success hazard entirely. Recommended.
  1. Skip the probe when a credential already exists — if \find-generic-password -s e -a t\ returns 0, the credential slot is occupied by a working item; the keychain is demonstrably writable. Skip the add-probe entirely for this case.
  1. Distinguish error codes — at minimum, special-case \-25299 errSecDuplicateItem\ in the error handler: "item already exists (you are logged in); keychain is writable." Only fall through to the unlock/change-password suggestion for genuine lock/auth errors (\errSecNoSuchKeychain\, \errSecAuthFailed\, etc.).
⚠️ Do NOT fix by adding \-U\ to the existing probe. That would make the probe's \add\ succeed (updating the real credential to a throwaway value) and then arm the \delete-on-success\ branch to delete the live OAuth credential — converting a harmless warning into credential loss on every doctor run.

Related

  • #69631 covers the same probe in a daemon/agent context (\-60008 errAuthorizationInternal\). That is a different error code and cause (GUI security session missing), but shares the same misleading fix suggestion. The throwaway-service-name fix above would address both.

View original on GitHub ↗