macOS: token refresh rewrites Claude Code-credentials in place and drops its keychain ACL, restarting the password prompt loop
Summary
On macOS, every OAuth token refresh rewrites the Claude Code-credentials keychain item in place and drops the item's ACL in the process. Any grant the user has given — via "Always Allow", or via a manual security set-generic-password-partition-list repair — is wiped, and the "security wants to access key 'Claude Code-credentials'" password prompt loop starts over roughly every few hours.
This is the same user-visible symptom as #89985, which covers why the prompt appears at all (credentials are read by forking /usr/bin/security, so securityd evaluates the ACL against a generic Apple tool). This issue is narrower: it isolates the write path as an independently fixable cause, with evidence that the item is updated rather than replaced.
Environment
- macOS 26.6.2 (25G83), Apple Silicon
- Claude Code 2.1.270 (bundled in Claude.app) and 2.1.271 (standalone CLI)
- Keychain item: service
Claude Code-credentials, login keychain (no-timeout, unlocked)
Evidence: the item is modified in place, not recreated
Across a token refresh, cdat is unchanged while mdat moves:
cdat = 20260908181554Z (unchanged across the refresh)
mdat = 20260915031337Z -> 20260915110920Z
A delete + re-add would produce a new cdat. So the write path reaches the existing item and drops its ACL along the way, rather than replacing the item wholesale. That suggests the fix may be small: preserve the existing SecAccess on update instead of writing a default one.
Reproduction
- Repair the grant on the item:
security set-generic-password-partition-list -S 'apple:,apple-tool:,teamid:XDX9UJP7V4' \
-s "Claude Code-credentials" -a <user> ~/Library/Keychains/login.keychain-db
- Confirm it worked — a scripted read returns the secret with no dialog:
security find-generic-password -s "Claude Code-credentials" -a <user> -w
- Wait for a token refresh (
mdaton the item advances). - Run the same read again. It now blocks on a password dialog.
In step 4 the test process died by SIGALRM (exit 142) with nothing on stderr — the dialog was open and waiting, not an error return.
Control
This account had a second credential item from another install (Claude Code-credentials-<hash>). Both items were repaired at the same moment. The second one has not been written since, and still reads silently; only the item that got rewritten regressed.
Same machine, same repair, same keychain state — the only difference is that one was rewritten. That isolates the regression to the write path.
Contributing factor: multiple credential items
Installs using different config dirs create separate Claude Code-credentials-<hash> items, each with its own ACL, so a grant given for one does nothing for the others. This account had three, one of them stale from an install that no longer existed. Combined with concurrent Claude Code processes each forking their own security (see #89985), a single expired grant produces ~10 simultaneous dialogs.
Expected
A token refresh should preserve the item's existing ACL, so that a grant the user has given survives until they revoke it.
Notes
There appears to be no supported way to opt out of the keychain on macOS — the binary only exposes --use-mock-keychain (test) and an internal allowUnrestrictedAccess for CI, neither configurable by users. Happy to capture securityd logs around a refresh if useful.
— samirhvbr
This issue has 2 comments on GitHub. Read the full discussion on GitHub ↗