[Bug] Keychain writability probe emits spurious warning when credential already exists (errSecDuplicateItem -25299)
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:
\\\jsmacOS Keychain is not writable (\${s})...\
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: \, 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
- Log in via \
claude /login\on macOS. - 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). - Observe: "macOS Keychain is not writable … errSecDuplicateItem … returned -25299" — even though auth works fine and the keychain is unlocked.
- Confirm: \
security find-generic-password -s "Claude Code-credentials"\→ item is there, healthy, \mdat\is recent.
Suggested fixes
- 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.
- 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.
- 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.