Claude Code Keychain Prompt Keeps Reappearing on macOS (ACL vs Partition List)¶
Symptom¶
A macOS dialog repeatedly pops up:
security wants to access key "Claude Code-credentials" in your keychain. To allow this, enter the "login" keychain password. —
[Always Allow] [Deny] [Allow]
The tell-tale sign: it comes back even after clicking "Always Allow" — the usual "trust forever" button doesn't make it stop. Login still works; it's the permission prompt that won't quiet down. This is distinct from Claude Code won't log in, where the stored credential is corrupt and login itself fails.
Cause¶
Claude Code stores its OAuth token in the macOS login keychain as Claude Code-credentials, read via /usr/bin/security. macOS binds an "Always Allow" grant (the keychain item's ACL) to the code-signing identity of the requesting binary. That grant is silently invalidated when:
- Claude Code updates — the new binary's signature no longer matches the saved ACL. This is the most common trigger (see claude-code issues #48162, #9403).
- ~~The credential item is recreated on token refresh — wipes the ACL.~~ Corrected 2026-09-04, see below — the item is not recreated (its creation date survives), and what a refresh resets is the partition list, not the ACL.
- Post-reboot keychain churn — right after boot, the just-unlocked login keychain plus a concurrent token refresh can race ahead of the ACL settling, producing a burst of prompts that stops once a clean refresh completes.
It is not a lock-timeout issue if security show-keychain-info reports no-timeout (below).
A keychain item has TWO independent gates (added 2026-09-04)¶
This is the thing that makes the problem look unfixable: passing the first gate does not get you past the second, and only the first is visible in Keychain Access.
| Gate | What it is | Where you see it | Repair |
|---|---|---|---|
| 1. ACL application list | which binaries may read the item | Keychain Access → item → Access Control | add the binary there, or click Always Allow |
| 2. Partition list | an additional XARA check by code-signing team | not shown in the GUI — security dump-keychain -a, description: line |
security set-generic-password-partition-list |
A binary can be listed and trusted under gate 1 and still be refused by gate 2. Gate 2 failures are silent — no dialog is drawn at all, which is why they present as mysterious forced re-logins rather than as prompts.
Read the current partition list (the entries render comma-separated on one line, so a lone apple-tool: is a real single-entry list, not a truncated render):
Root cause of the recurring case: the credential write strips the partition list¶
Measured on macOS 27, Claude Code 2.1.260, 2026-09-04:
- partition list restored to
apple:, apple-tool:, teamid:<TEAMID>and verified - a new session's login wrote the credential (modification date moved, creation date unchanged — an in-place update, not a delete-and-recreate)
- 17 seconds later the list was back to
apple-tool:— both other entries gone
A verified-good state survived five idle minutes and died seconds after a write. Saving the credential is what removes permission to read it back, so the loop is self-sustaining: login saves → save strips → later read fails → forced login → saves again.
Two theories this kills, both of which are easy to arrive at independently:
- It is not the installer. A package update that does not write the credential leaves the list intact. Installs only appear correlated because a login or refresh often happens near one.
- It is not a nightly/scheduled job. OAuth refreshes are timer-driven and land at arbitrary hours — a strip at 00:08 looks exactly like a cron job and is not one. Do not go hunting through
periodic, Spotlight, or sleep/wake logs.
⚠️ Honest limit: that a stripped partition list by itself breaks Claude Code is not proven. Claude Code reads through
/usr/bin/security, whichapple-tool:still covers, and a read probe can return success while the list is stripped. Before concluding the partition list is your problem, confirm you actually get forced re-logins while it is stripped.
Triage (non-destructive — these do not trigger a prompt)¶
# Confirm the item exists (metadata only; no secret read)
security find-generic-password -l "Claude Code-credentials" | grep -E "svce|acct"
# Confirm the login keychain isn't auto-locking
security show-keychain-info ~/Library/Keychains/login.keychain-db
# -> "no-timeout" means it won't relock; so recurring prompts = ACL invalidation, not locking
Fixes¶
One-off burst (e.g. right after a reboot)¶
Click Always Allow (not Allow) once a clean token refresh has completed. With a no-timeout keychain the grant then holds, and the post-boot prompt storm usually self-clears within a minute. Observed exactly this on MajorAir 2026-06-15 — a reboot triggered a burst that stopped on its own.
Keeps returning after updates (durable) — reset the credential¶
Deleting and re-creating the item rebinds a fresh ACL to the current binary. Costs one re-login.
security delete-generic-password -s "Claude Code-credentials"
# then re-authenticate inside Claude Code: /login (or relaunch `claude`)
Restore the partition list (gate 2)¶
Interactive — it asks for the login keychain password, so run it in your own terminal, never inside an agent session whose transcript keeps the input.
security set-generic-password-partition-list \
-S apple:,apple-tool:,teamid:<YOUR_TEAMID> \
-s "Claude Code-credentials" -a "$USER"
apple-tool: is what lets /usr/bin/security read without prompting; the teamid: entry is what authorizes the vendor's own binary. Restoring only apple-tool: silences the dialog while leaving the silent failure in place — an easy and misleading partial fix.
⚠️ This does not survive the next credential write. Per the root-cause section, a login or token refresh strips it again. Treat it as symptom relief, not a fix.
The prompt won't go away even after the permission is fixed — SecurityAgent queues them¶
If pressing esc dismisses the dialog and another appears instantly, you are not seeing one stubborn dialog or a live requester — you are draining a backlog. SecurityAgent accumulates every unanswered prompt, and anything polling the item on a timer manufactures several per hour while the permission is broken.
ps -Ao pid,lstart,etime,command | grep '[S]ecurityAgent' # long uptime = holding a queue
kill -9 <pid> # SIGTERM is IGNORED; launchd respawns a clean agent in seconds
SecurityAgent's uptime instead.
Find out what is actually asking¶
The dialog names the requesting process but not its parent, which is what you usually need. Stream it:
log stream --predicate 'process == "securityd" AND eventMessage CONTAINS "displaying keychain prompt"' --style compact
ps -o ppid=). This routinely shows the asker is your own tooling — a monitoring script, or the agent session you are debugging from — rather than an unknown third party.
Bypass the keychain entirely (workaround)¶
Claude Code falls back to ~/.claude/.credentials.json in non-GUI contexts (SSH, tmux). On a local Mac this can be repurposed to stop keychain prompts for good:
# pipe straight to the file — never echo the token into a shared terminal
security find-generic-password -s "Claude Code-credentials" -w > ~/.claude/.credentials.json
chmod 600 ~/.claude/.credentials.json
security delete-generic-password -s "Claude Code-credentials"
CLAUDE_CODE_OAUTH_TOKEN — it is known to delete credentials on exit (issue #37512).
Notes¶
- Same keychain item as the corrupt-credential login failure; if login itself breaks, see the related article.
- Always redirect
-woutput straight to a file — never into a terminal whose scrollback feeds shared context.
Related¶
- Claude Code Won't Log In (Warp & iTerm2) — Corrupt Keychain Credential
- Config:
~/.claude.json, login keychain itemClaude Code-credentials - First observed: MajorAir, 2026-06-15 (post-reboot prompt burst; self-cleared)
- Root cause measured: MajorM5Pro, 2026-09-04 (macOS 27, Claude Code 2.1.260) — credential write strips the partition list
- If you monitor the partition list on a timer, compare against the expected team ID per item, not a count of
teamidoccurrences — a wrong vendor's team ID otherwise reads as a valid grant. And route alerts somewhere durable: a notification that fires at 00:08 is a notification nobody sees.