fix(statusline): handle suffixed Claude Code keychain service names#578
fix(statusline): handle suffixed Claude Code keychain service names#578halindrome wants to merge 12 commits into
Conversation
Older Claude Code installs persisted OAuth credentials under the literal keychain service name "Claude Code-credentials". Newer installs use per-install suffixed names like "Claude Code-credentials-<8hex>". The statusline only queried the literal name, so on those installs Priority 2 (macOS keychain) always returned errSecItemNotFound. With no ".credentials.json" fallback present and "claude auth status --json" reporting authMethod=claude.ai, control reached lines 956-970 which emitted "Limits: keychain access denied" — wrongly blaming a permissions denial that never happened. This change: - Tries the legacy literal service name first (back-compat). - On miss, enumerates suffixed candidates via a non-prompting "security dump-keychain" (no -d, so secrets are not dumped and no GUI prompt fires), iterates them, takes the first whose JSON yields .claudeAiOauth.accessToken. - Replaces the misleading "keychain access denied" wording with an honest "OAuth token unavailable (set VBW_OAUTH_TOKEN)" — the original message suggested a fix (allow Terminal in Keychain Access.app) that cannot resolve item-not-found. - Adds tests/statusline-keychain-suffixed.bats covering literal-name back-compat, suffixed-name discovery, and the new fallback message. Linux (secret-tool/pass) is unchanged; if needed, file a follow-up. Fixes swt-labs#576 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
QA Round 1 |
Addresses major and minor findings from QA round 1 on PR swt-labs#578. Major: - Multi-install non-determinism (try-all-on-401). With multiple suffixed Claude Code-credentials-<8hex> entries (the swt-labs#576 reporter has six), the prior change picked the first by sort -u and was stuck on a stale token if that install's grant was revoked — pinned to "Limits: auth expired (run /login)" until cache expiry. Now collects all valid suffixed tokens into _KEYCHAIN_FALLBACK_TOKENS, then in the usage-fetch block iterates candidates: 401/403 retries with the next, while 429 and other failures break immediately (retrying wouldn't help). - Exit-code-aware messaging (deferred — design choice, not bug). The QA agent flagged that the issue's proposed fix included branching on exit 44 (errSecItemNotFound) vs exit 51 (errSecAuthFailed). That distinction was deliberately collapsed during planning to avoid bumping the slow-cache schema (15→16 fields with back-compat). The current "OAuth token unavailable (set VBW_OAUTH_TOKEN)" wording is honest for both sub-cases — for genuine ACL denial the env-var workaround still works, and the user can run `security find-generic-password -g 'Claude Code-credentials'` themselves to see the underlying error. Documented inline in the message-branch comment. Minor: - CRED_JSON hygiene: unset after the macOS keychain block so the credential JSON (which includes the refresh token) does not linger in the script's variable namespace. - Test mock fidelity: mock `security dump-keychain` now emits realistic multi-line output with leading indentation and surrounding attribute lines, so a future regression that requires beginning-of- line anchoring on the parser regex would be caught. - Positive assertion in suffixed-discovered test: mock curl records the Bearer token to `.curl-bearer`; the test asserts the suffixed lookup actually flowed `fake-token-suffixed` to the API call, not just that L3 happens to render without the unavailable message. - New BATS case for the multi-install try-all-on-401 path: two suffixed entries, mock curl rejects the first (401) and accepts the second; test asserts both tokens were tried in order and the final L3 is not an auth-expired message. Linux follow-up filed separately for suffixed-name parity in secret-tool / pass paths. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
QA Round 2Maintainer notes on this round
|
Round 2 raised four minor findings (one new portability concern, three
polish/documentation items). The bearer-order assertion the agent
flagged as "hypothetical" was already present in the multi-install BATS
test (`token-stale-primary` then `token-fresh-fallback` read from
.curl-bearer-log) — no change needed. The dump-keychain mock fidelity
finding was also already addressed in round 1 (multi-line keychain:/
class:/attributes: framing).
Changes:
- Portable mapfile replacement. `_install_mock_security_multi` used
`mapfile -t` which is bash 4+. macOS default `/bin/bash` is 3.2 and
the mock's shebang is `/usr/bin/env bash`, so a contributor running
BATS locally on macOS without homebrew bash would silently fail.
Replaced with `while IFS= read -r line; do arr+=("$line"); done`
loops — works on bash 3.2 through 5.x.
- Inline note on the deferred exit-44/51 distinction. Added a comment
block at the FETCH_OK=noauth + AUTH_METHOD=claude.ai branch
explaining that collapsing the two error sub-cases into a single
honest message was a deliberate design choice (cache schema bump
avoidance) and pointing future maintainers at PR swt-labs#578 if they want
to revisit it. Without this comment, the next round of automated
review would re-flag the same finding.
- CRED_JSON unset symmetry. The round 1 fix added `unset CRED_JSON`
on the macOS keychain branch only. Hoisted it past the
`if Darwin … else Linux … fi` so both branches benefit, with a brief
hygiene comment.
- Candidate count cap. Bounded the try-all-on-401 loop to 4 attempts
total (primary + 3 alternates) to avoid pathological 18-second
blocks for users with many stale installs (3s curl timeout × 6).
Pre-existing issues: none discovered.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
QA Round 3QA loop complete per CONTRIBUTING (3 rounds, latest clean). |
|
@halindrome you need to follow the contributing guide in addition to making sure all tests pass |
…laude-code-vbw into fix/576-statusline-keychain-suffixed-service-name
Round 3 fresh review (post-merge with current main) is CLEAN FOR MERGE: 4/4 keychain-suffixed tests pass, full suite green (3595 BATS, 51/51 contract, lint), shellcheck clean, no set -e/-u/pipefail footguns. The one optional regex-tightening nit was deliberately not applied to avoid false negatives on non-hex install suffixes; over-match risk is negligible (match anchored to the full 'Claude Code-credentials-' prefix). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
QA Review — Round 3 (fresh read-only review of the #576 fix against base Disposition of the optional nit: the reviewer suggested tightening |
Linux CI revealed that the swt-labs#576 keychain-suffixed tests could not run on the CI runner: the security-based discovery is (correctly) gated on _OS=Darwin, so on Linux the branch was skipped and the mocked security/ curl were never invoked, leaving tests 2 and 3 failing on absent .curl-bearer markers. Rounds 1-3 ran on macOS where the path executes, so the gap was invisible. Test-only fix: add _install_mock_uname_darwin (mock uname -> Darwin, delegating other flags to the real binary) and invoke it from the two security-mock helpers, so the macOS keychain path is exercised on Linux runners. No production code changed; the OS gate is correct and the Linux secret-tool/pass branch is untouched. Reproduced with a uname->Linux shim (marker absent) vs uname->Darwin (marker present); full suite green (3595 BATS, 51/51 contract, lint). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
QA Review — Round 4 (triggered by Linux CI failure that the macOS-only round-3 review could not observe) What CI revealed: after round 3, the Bats Fix (test-only, Reproduction + verification (deterministic): with a Addressed in this |
dpearson2699
left a comment
There was a problem hiding this comment.
Verdict: REQUEST_CHANGES
Blocking findings are inline where GitHub could anchor them.
Unanchored blockers:
- None.
Evidence:
- Linked issue: #576.
- Tests:
bash testing/run-all.shwas attempted from the PR worktree and hung intests/statusline-keychain-suffixed.bats; focusedbash -n tests/statusline-keychain-suffixed.batsfailed withline 256: syntax error near unexpected token '}'; focusedbats --filter 'multi-install' tests/statusline-keychain-suffixed.batshung inside_install_mock_security_multiand was interrupted. - Blind baseline: completed; expected suffixed keychain discovery plus distinct rc 44 vs rc 51 messaging and tests for literal, suffixed, rc 44, and rc 51 paths.
- QA: completed; confirmed VBW-PR-001 and VBW-PR-002 as blocking.
Review marker: VBW-PR-578-inline-review-v2
| # cache (15 → 16 fields with back-compat read), and the env-var workaround | ||
| # already resolves both sub-cases. Future maintainers: if you want exit-code | ||
| # branching, plan the cache schema bump first. See PR #578. | ||
| USAGE_LINE="${D}Limits: OAuth token unavailable (set VBW_OAUTH_TOKEN)${X}" |
There was a problem hiding this comment.
VBW-PR-001 (blocker): Issue #576 requires exit-code-aware keychain messaging: rc 44 should render the OAuth-token-unavailable message with /login guidance, while rc 51 should keep the existing keychain-access-denied wording. This branch always renders the generic unavailable message after explicitly collapsing rc 44 and rc 51 above, and the slow cache does not preserve keychain status. Carry the keychain result through cache/rendering instead of collapsing these cases.
| local tokens="$3" # newline-separated tokens, parallel to names | ||
| mkdir -p "$fake_bin" | ||
| _install_mock_uname_darwin "$fake_bin" | ||
| cat > "$fake_bin/security" <<SH |
There was a problem hiding this comment.
VBW-PR-002 (blocker): This generated-script heredoc is unquoted, so shell expansion runs while the test is building the fake security script. The backticked mapfile text below is treated as command substitution, which makes the focused multi-install BATS case hang before the fake script is written; bash -n tests/statusline-keychain-suffixed.bats also fails. Quote this heredoc or escape every expansion point so the full suite can complete.
swt-labs#576) Addresses PR swt-labs#578 review (VBW-PR-001, VBW-PR-002). VBW-PR-001: the claude.ai-login-but-no-token branch collapsed errSecItemNotFound (exit 44) and errSecAuthFailed (exit 51) into one generic message, and the slow cache did not carry keychain status, so a real access denial was no longer reported. Capture the `security` exit code at both the literal and suffixed lookups, thread it through the slow cache as a 16th field (back-compat read: older 15-field caches yield an empty status), and branch rendering: exit 51 keeps the "keychain access denied (allow Terminal in Keychain Access.app ...)" wording; everything else (exit 44 / non-Darwin) renders "OAuth token unavailable (run /login or set VBW_OAUTH_TOKEN)". Adds a focused exit-51 test. VBW-PR-002: tests/statusline-keychain-suffixed.bats built its multi-install `security` mock with an unquoted heredoc, so a backticked word in a comment ran as a command substitution at write time — it read stdin and hung, leaving a zero-byte mock (the full suite hung here). Reworded the comment backtick-free and documented the constraint inline; the new exit-51 mock uses a quoted heredoc to show the safe pattern. Tests: bash testing/run-all.sh green (3596 BATS, 0 failed); the keychain file is 5/5, including the previously-hanging multi-install case and the new exit-51 case. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QFFmV2TntcmvJRjvcv3gAD
|
Addressed the review feedback in VBW-PR-001 — exit-code-aware keychain messaging. The
VBW-PR-002 — BATS hang. Root cause: the multi-install mock used an unquoted heredoc, so a backticked word in a comment ran as a command substitution at mock-build time — it read stdin and hung, leaving a zero-byte Also added the missing exit-51 test alongside the existing exit-44 case.
|
…keychain-suffixed-service-name
QA Round 5Model used: Claude Opus 4.8 (read-only What was tested
Expected vs actual
Findings
VerdictPASS (clean — only minor/cosmetic observations). Both prior-review blockers (VBW-PR-001 and VBW-PR-002) independently confirmed fully fixed; all five required test cases exist and pass; slow-cache field ordering correct (16=16); legacy caches degrade safely; no regression in adjacent statusline tests; shellcheck clean. Branch was also re-synced with QA performed by Claude Code (claude-opus-4-8) |
…west-first) The swt-labs#576 suffixed-name fallback collected tokens in service-name sort order and used index [0] as primary. A user with multiple Claude Code installs (common: a stale token plus the live one) could therefore try a stale token first on every render — a guaranteed 401 before falling through to the live token, doubling usage-API calls and adding rate-limit pressure. Order candidates by the stored OAuth expiresAt (ms epoch) descending so the freshest install is tried first; tokens without a usable expiresAt sort as 0 (last). The 401-fallthrough retry path is unchanged — only ordering improves. Tests: parameterize the multi-install mock with expiresAt; make the existing fallthrough test deterministic under the new ordering; add a test asserting the highest-expiresAt token is used even when it sorts last lexically. Refs swt-labs#576
QA Round 6Model used: Claude Opus 4.8 (read-only Why this changeThe suffixed-name fallback (#576) collected tokens in service-name (lexical) order and used index What was tested (on macOS bash 3.2.57 — production target)
Expected vs actual (devil's-advocate)
FindingsContract: none. Regression: none. Schema change: none. critical 0 / major 0 / minor 0. VerdictPASS — no blocking items. Commit Full local suite green (EXIT=0). Branch is current with QA performed by Claude Code (claude-opus-4-8) |
What
Fix #576 — VBW statusline's Limits line wrongly shows "keychain access denied" on macOS when Claude Code stores OAuth credentials under per-install suffixed service names (e.g.
Claude Code-credentials-126018b2).Why
scripts/vbw-statusline.shPriority 2 only queried the literal service nameClaude Code-credentials. On affected installs the keychain has no item under that exact name (securityreturns exit 44,errSecItemNotFound), so the lookup silently returned empty. With no.credentials.jsonfallback file present andclaude auth status --jsonreportingauthMethod=claude.ai, control reached lines 956–970 which emittedLimits: keychain access denied (allow Terminal in Keychain Access.app or set VBW_OAUTH_TOKEN)— a misleading message that:How
Three changes in
scripts/vbw-statusline.sh, plus extensive BATS coverage. Built up across the original commit and two QA-driven follow-ups.1. Suffixed-name lookup (Priority 2, macOS branch)
Claude Code-credentialsfirst (back-compat for older installs).Claude Code-credentials-<suffix>candidates via a non-promptingsecurity dump-keychainfilter (no-d, so secrets are not dumped and no GUI prompt fires)..claudeAiOauth.accessTokenfrom each — collect all valid tokens into_KEYCHAIN_FALLBACK_TOKENS.2. Multi-install try-all-on-401 fallback (added in QA round 1)
The original commit picked the lexically-first valid token. The #576 reporter has six suffixed entries; if the chosen install's token is stale, the user would have been pinned to
Limits: auth expired (run /login)until cache expiry — even though another install holds a fresh token._KEYCHAIN_FALLBACK_TOKENS, deduped).401/403continue to the next candidate;429and other failures break immediately (retrying wouldn't help).3. Honest fallback message (line 964–965)
Replaced
Limits: keychain access denied (...)withLimits: OAuth token unavailable (set VBW_OAUTH_TOKEN)— the branch fires whenever priorities 1–3 all return empty ANDclaude auth statusconfirms an OAuth login, regardless of why the keychain didn't yield a token.Linux paths (
secret-tool/pass) are unchanged — the upstream Claude Code suffix scheme on Linux needs separate confirmation. Tracked as #580 follow-up.Test plan
tests/statusline-keychain-suffixed.bats(4 cases, all passing):Claude Code-credentials-<hex>is discovered when the literal is absent — asserts the suffixed token actually flowed to the curl call.claude.ailogin + no reachable token emits the new "OAuth token unavailable" message and no longer says "keychain access denied".tests/statusline-*.batsstill pass.bash testing/run-all.shclean across all three commits: 1/1 lint, 51/51 contracts, 3318/3318 BATS, zero failures.Notes
[[ ! "$str" =~ "..." ]]rather than! grep -q— the latter does not fail BATS tests (SC2314), so the inverse-grep pattern would silently no-op.security dump-keychain(no-d) is non-prompting and dumps only metadata; it does not trigger a Keychain Access dialog and does not read secret payloads.while IFS= read -rloop instead ofmapfile -t(bash 4+ only) so contributors running BATS on macOS default/bin/bash3.2 are covered.QA review log
Per CONTRIBUTING.md, each round's report has been posted as a separate PR comment.
03aa03da) — addressed multi-install non-determinism via try-all-on-401, plus minor polish.e196d758) —mapfileportability fix, candidate-count cap, CRED_JSON unset symmetry, inline design-choice note.🤖 Generated with Claude Code