Skip to content

Claude: adopt a valid credential when the delegated-refresh touch changed nothing - #2739

Closed
luisgonzaleznf wants to merge 4 commits into
steipete:mainfrom
luisgonzaleznf:fix/claude-keychain-observation-false-negative
Closed

Claude: adopt a valid credential when the delegated-refresh touch changed nothing#2739
luisgonzaleznf wants to merge 4 commits into
steipete:mainfrom
luisgonzaleznf:fix/claude-keychain-observation-false-negative

Conversation

@luisgonzaleznf

@luisgonzaleznf luisgonzaleznf commented Aug 7, 2026

Copy link
Copy Markdown

Scope corrected after review. This PR originally claimed it restored background recovery for an expired Claude OAuth credential. That was wrong for the default configuration, and I've rewritten the title and this body to describe what it actually does. The original framing is preserved in the comments below rather than quietly edited away. Details in the correction comment.

What this fixes

ClaudeUsageFetcher.loadAfterDelegatedRefresh gated the silent keychain re-sync on the delegated-refresh outcome:

let didSyncSilently = delegatedOutcome == .attemptedSucceeded
    && ClaudeOAuthCredentialsStore.syncFromClaudeKeychainWithoutPrompt(...)

The touch changes nothing when the Claude CLI's own token is still valid, so the keychain-change probe reports no change and the attempt is recorded .attemptedFailed. The && then short-circuits, skipping the step that would adopt the valid credential already present in the keychain. The subsequent retry at ClaudeUsageFetcher.swift:464 runs with allowKeychainPrompt: false and so cannot benefit from a credential the sync never loaded.

syncFromClaudeKeychainWithoutPrompt passes allowKeychainPrompt: false on every read (ClaudeOAuthCredentials.swift:1219, :1288, :1316), its candidates probe is no-UI (:2360-2369), and it only adopts a credential that is not expired (:1225, :1296, :1321). Running it when the outcome was .attemptedFailed therefore cannot prompt and cannot regress a working account.

What this does NOT fix

This does not restore background recovery under the default prompt policy, and the original version of this PR wrongly implied it did.

With claudeOAuthKeychainPromptMode at its default .onlyOnUserAction, a background refresh throws at ClaudeUsageFetcher.swift:404assertDelegatedRefreshAllowedInCurrentInteraction, with allowBackgroundDelegatedRefresh hardcoded false at ClaudeProviderDescriptor.swift:676 — long before reaching the sync at :440. syncFromClaudeKeychainWithoutPrompt self-gates on the same policy anyway (shouldAllowClaudeCodeKeychainAccess: .onlyOnUserAction returns ProviderInteractionContext.current == .userInitiated).

So the effect of this PR is:

prompt mode effect
.always background recovery works where it previously did not
.onlyOnUserAction (default) the post-delegation retry can now find an adopted credential instead of none; the background gate above is untouched
.never no change

Two further scope limits, for accuracy: in default Auto mode oauthKeychainPromptCooldownEnabled is true (ClaudeProviderDescriptor.swift:674), so .skippedByCooldown / .skippedByPromptPolicy / .cliUnavailable still throw at :419-427 before the sync — "runs on every outcome" was overstated. And saveClaudeKeychainFingerprint inside the sync now fires on paths where it previously did not.

Making the default configuration self-heal is a separate, larger change: it means letting a provably-non-prompting read run under .onlyOnUserAction. That is a policy decision I don't think a drive-by PR should make — I tried relaxing an adjacent prompt gate earlier in this branch and it caused 8 test regressions, which is a fair warning about that surface.

Commits

  • 508c9f3 — the fix: drop the && gate on the silent sync.
  • 653816cwaitForClaudeKeychainChange returned Bool, collapsing "could not read the keychain" into "did not change". Now a tri-state. Honest caveat: with the ordering below, the practical effect is limited to log and message text — call it tidying rather than hardening, and it is separable if you'd rather not carry it.
  • 695f478 — review fix. Both bots caught that the indeterminate branch preceded isRefreshResultUnreadable; an unreadable profile satisfies both at once, which dropped isUnreadableAfterRefresh (losing the "switch source" guidance) and took the 20s cooldown instead of 5m, restoring the CLI relaunch loop Report unreadable Claude OAuth refresh as terminal #2650 removed. Precedence is now unreadabletouchError.indeterminate.unchanged.
  • c9775a2make check naming violations.

Testing

  • swift test --filter ClaudeOAuthDelegatedRefresh29/29.
  • make check0 violations across 1806 files.
  • The regression test for 695f478 is verified to actually catch its defect: against the pre-fix ordering it fails with isUnreadableAfterRefresh: false and the wrong message string.
  • No regressions vs main at 22b24b8. Residual failures under a wide filter come from suites that are order-dependent under parallel execution (ClaudeOAuthPromptCoalescingTests, ClaudeCLISessionTests, ClaudeLoginRunnerTests, ClaudeCLITimeoutRetryTests, ClaudeOAuthDelegatedRefreshProfileIsolationTests); the set differs between runs of the same commit, each also fails on main, and all pass in isolation.

Known gap: the 508c9f3 change itself has no direct test — restoring the && would still pass the suite. The existing tests cover the tri-state classifier and the 695f478 ordering. Worth fixing before merge; flagging it rather than leaving it to be discovered.

Verification I could not do

I have not observed a card recover with this patch in the release-signed keychain state. A locally built app is ad-hoc signed and cannot durably read CodexBar's own ACL-bound cache items (Keychain cache item is unusable by this executable (oauth.claude.profile.<hash>)), so the precondition can't be reproduced from a fork. That check needs the release signing identity.

Prior traces in this thread are pre-fix, from a Developer ID signed 0.48.0: a credential ~44h expired with hasRefreshToken=true and source=cacheKeychain, delegating every cycle without recovering. They establish the symptom on current release; they do not by themselves establish the mechanism, which is reasoning from the code paths cited above.

When the Claude CLI's own token is still valid, the delegated-refresh touch
correctly changes nothing — so the keychain-change probe reports no change and
the attempt is recorded as `.attemptedFailed`. The silent re-sync was gated on
`.attemptedSucceeded`, so that outcome skipped the one step that would have
adopted the valid credential already sitting in the keychain.

The account then stayed pinned to its expired cached token, showing an error
that blamed the Keychain prompt policy, until the user pressed Refresh by hand
— a user-initiated read takes a different path and recovers.

syncFromClaudeKeychainWithoutPrompt passes allowKeychainPrompt: false on every
read and only adopts a credential that is not expired, so running it on any
outcome cannot prompt and cannot regress a working account.
waitForClaudeKeychainChange returned Bool, so "could not read the keychain"
and "the keychain did not change" collapsed into the same `false`. Those need
opposite responses: the second is a real refresh failure, the first says
nothing at all and should stay retryable.

Observation is now a tri-state. An indeterminate reading records the short
cooldown and reports an accurate message instead of asserting the touch
failed to update anything.

The per-baseline nil semantics are preserved deliberately: Security.framework
observation still treats a missing baseline as movement, while security-CLI
observation still treats it as inconclusive rather than letting a later
successful read masquerade as a change.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 653816c54a

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

@clawsweeper clawsweeper Bot added rating: 🦪 silver shellfish Thin PR readiness signal; proof, validation, or implementation needs work. status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. P2 Normal priority bug or improvement with limited blast radius. merge-risk: 🚨 auth-provider 🚨 Merging this PR could break OAuth, tokens, provider routing, model choice, or credentials. labels Aug 7, 2026
@clawsweeper

clawsweeper Bot commented Aug 7, 2026

Copy link
Copy Markdown

Codex review: needs real behavior proof before merge. Reviewed August 7, 2026, 6:58 PM ET / 22:58 UTC.

ClawSweeper review

What this changes

This PR always attempts a non-interactive Claude Keychain credential re-sync after delegated refresh and records unreadable Keychain observations separately from unchanged entries.

Merge readiness

Blocked until real behavior proof is added - 5 items remain

Keep open, but do not merge yet: the source change is narrowly plausible, while its central no-change adoption path lacks a regression test and an after-fix release-signed runtime trace.

Priority: P2
Reviewed head: c9775a223e010834a00e73f05a2d0174d1e53744

Review scores

Measure Result What it means
Overall readiness 🧂 unranked krab (1/6) The narrowed implementation is plausible, but its defining behavior lacks both a direct regression and real after-fix proof.
Proof confidence 🧂 unranked krab (1/6) Needs real behavior proof before merge: Focused tests and a pre-fix release trace are useful, but the PR has no redacted after-fix release-signed runtime result for the affected Keychain state. After adding proof, update the PR body; ClawSweeper should re-review automatically. If it does not, the PR author or someone with repository write access can comment @clawsweeper re-review.
Patch quality 🦐 gold shrimp (3/6) 1 actionable review finding remain.

Verification

Check Result Evidence
Real behavior Needs proof Needs real behavior proof before merge: Focused tests and a pre-fix release trace are useful, but the PR has no redacted after-fix release-signed runtime result for the affected Keychain state. After adding proof, update the PR body; ClawSweeper should re-review automatically. If it does not, the PR author or someone with repository write access can comment @clawsweeper re-review.
Evidence reviewed 4 items Current main retains the failing gate: The current retry only invokes the silent credential re-sync when the delegated outcome is attemptedSucceeded, so attemptedFailed skips adoption before the non-interactive retry.
Silent sync is non-interactive and filters expired credentials: The sync checks prompt policy with allowKeychainPrompt false and only writes a credential after parsing it as non-expired.
The proposed behavior has no direct regression coverage: The existing store-backed recovery test covers attemptedSucceeded, while the PR body confirms that reverting its attemptedFailed sync change would still pass the suite.
Findings 1 actionable finding [P2] Cover the no-change credential-adoption path
Security None None.

How this fits together

CodexBar’s Claude usage fetcher delegates expired OAuth refreshes to Claude Code, then attempts to reload a usable credential for the usage retry. This path determines whether a valid Keychain credential can replace an expired cached credential without prompting.

flowchart LR
A[Expired cached credential] --> B[Delegated Claude refresh]
B --> C[Keychain observation]
C --> D[Silent credential re-sync]
D --> E[Usage retry]
C --> F[Unreadable-source recovery guidance]
Loading

Before merge

  • Add real behavior proof - Needs real behavior proof before merge: Focused tests and a pre-fix release trace are useful, but the PR has no redacted after-fix release-signed runtime result for the affected Keychain state. After adding proof, update the PR body; ClawSweeper should re-review automatically. If it does not, the PR author or someone with repository write access can comment @clawsweeper re-review.
  • Cover the no-change credential-adoption path (P2) - The ungated sync is the central fix for .attemptedFailed, but neither added test reaches this retry path with that outcome and a valid Keychain credential; the existing store-backed recovery test returns .attemptedSucceeded. This coverage gap was visible on the prior reviewed head and is raised late here because the PR now explicitly confirms reverting this call still passes. Add a focused attemptedFailed/no-change test that proves the fresh credential is used.
  • Resolve merge risk (P1) - The PR changes OAuth credential selection after every attempted no-change refresh, but no focused regression proves that a valid Keychain credential is adopted specifically from attemptedFailed.
  • Resolve merge risk (P1) - The submitted evidence has no after-fix run in the release-signed Keychain state; the default background prompt policy exits before this code path, so the affected configuration needs explicit proof.
  • Complete next step (P2) - A maintainer or contributor with release-signing access must provide real behavior proof; the direct coverage finding should also be addressed before merge.

Findings

  • [P2] Cover the no-change credential-adoption path — Sources/CodexBarCore/Providers/Claude/ClaudeUsageFetcher.swift:440-443
Agent review details

Security

None.

Review metrics

Metric Value Why it matters
Production versus test delta production +65/-34, tests +82/-1 across 4 files Most of the patch is focused test coverage, but none exercises the primary attemptedFailed credential-adoption change.

Merge-risk options

Maintainer options:

  1. Prove the no-change adoption path before merge (recommended)
    Add a focused regression for a valid Keychain credential after attemptedFailed and attach a redacted release-signed after-fix trace.
  2. Pause pending signed-runtime validation
    Leave the PR open until someone with the release signing identity can demonstrate the affected Keychain state and recovery.

Technical review

Best possible solution:

Retain the narrow non-prompting sync change only with a focused attemptedFailed/no-change regression and redacted release-signed evidence showing that the fresh credential is selected without prompting.

Do we have a high-confidence way to reproduce the issue?

Yes for a source-level reproduction: the existing recovery harness can seed an expired cache and valid Keychain override, return attemptedFailed with no fingerprint movement, and verify use of the fresh credential. No after-fix release-signed runtime reproduction is attached.

Is this the best way to solve the issue?

Yes, conditionally: removing the outcome gate is the narrowest way to attempt the already non-interactive sync, but it needs direct regression coverage and signed-runtime proof before merge.

Full review comments:

  • [P2] Cover the no-change credential-adoption path — Sources/CodexBarCore/Providers/Claude/ClaudeUsageFetcher.swift:440-443
    The ungated sync is the central fix for .attemptedFailed, but neither added test reaches this retry path with that outcome and a valid Keychain credential; the existing store-backed recovery test returns .attemptedSucceeded. This coverage gap was visible on the prior reviewed head and is raised late here because the PR now explicitly confirms reverting this call still passes. Add a focused attemptedFailed/no-change test that proves the fresh credential is used.
    Confidence: 0.98
    Late finding: first raised on code an earlier review cycle already covered.

Overall correctness: patch is incorrect
Overall confidence: 0.93

AGENTS.md: found and applied where relevant.

Codex review notes: model internal, reasoning high; reviewed against 2436b9f95c8f.

Labels

Label changes:

  • add rating: 🧂 unranked krab: Overall readiness is 🧂 unranked krab; proof is 🧂 unranked krab and patch quality is 🦐 gold shrimp.
  • remove rating: 🦪 silver shellfish: Current PR rating is rating: 🧂 unranked krab, so this older rating label is no longer current.

Label justifications:

  • P2: This is a bounded Claude OAuth recovery defect with user-visible retry impact, not an emergency outage.
  • merge-risk: 🚨 auth-provider: The patch changes when CodexBar adopts and caches Claude OAuth credentials after delegated refresh.
  • rating: 🧂 unranked krab: Overall readiness is 🧂 unranked krab; proof is 🧂 unranked krab and patch quality is 🦐 gold shrimp.
  • status: 📣 needs proof: The PR needs real behavior proof before ClawSweeper can clear the contributor ask. Needs real behavior proof before merge: Focused tests and a pre-fix release trace are useful, but the PR has no redacted after-fix release-signed runtime result for the affected Keychain state. After adding proof, update the PR body; ClawSweeper should re-review automatically. If it does not, the PR author or someone with repository write access can comment @clawsweeper re-review.

Evidence

What I checked:

Likely related people:

  • Peter Steinberger: Current-main blame attributes the delegated-refresh retry and its outcome gate to the v0.48.0 source commit. (role: current source introducer; confidence: medium; commits: 5bd587850611; files: Sources/CodexBarCore/Providers/Claude/ClaudeUsageFetcher.swift, Sources/CodexBarCore/Providers/Claude/ClaudeOAuth/ClaudeOAuthDelegatedRefreshCoordinator.swift)
  • kes02: The merged delegated-refresh terminal-state work is the directly adjacent behavior this PR preserves and extends. (role: adjacent merged feature contributor; confidence: medium; commits: 5871d2036fc3; files: Sources/CodexBarCore/Providers/Claude/ClaudeOAuth/ClaudeOAuthDelegatedRefreshCoordinator.swift)

Rank-up moves

Optional improvements that raise the rating; they are not merge blockers.

  • Add the focused attemptedFailed/no-change credential-adoption regression.
  • Attach redacted release-signed runtime logs or terminal output showing the post-fix recovery.

Rating scale

Score Internal tier Crab rank Meaning
6/6 S 🦀 challenger crab Exceptional readiness
5/6 A 🦞 diamond lobster Very strong readiness
4/6 B 🐚 platinum hermit Good normal PR; ordinary maintainer review
3/6 C 🦐 gold shrimp Useful, but confidence is limited
2/6 D 🦪 silver shellfish Proof or implementation needs work
1/6 F 🧂 unranked krab Not merge-ready
N/A NA 🌊 off-meta tidepool Rating does not apply

Overall follows the weaker of proof and patch quality.
Shiny media proof means a screenshot, video, or linked artifact directly shows the changed behavior. Runtime, network, CSP, and security claims still need visible diagnostics.

Workflow

  • ClawSweeper keeps one durable marker-backed review comment per issue or PR.
  • Re-runs edit this comment so the latest verdict, findings, and automation markers stay together instead of adding duplicate bot comments.
  • A fresh review can be triggered by eligible @clawsweeper re-review comments, exact-item GitHub events, scheduled/background review runs, or manual workflow dispatch.
  • PR/issue authors and users with repository write access can comment @clawsweeper re-review or @clawsweeper re-run on an open PR or issue to request a fresh review only.
  • Maintainers can also comment @clawsweeper review to request a fresh review only.
  • Fresh-review commands do not start repair, autofix, rebase, CI repair, or automerge.
  • Maintainer-only repair and merge flows require explicit commands such as @clawsweeper autofix, @clawsweeper automerge, @clawsweeper fix ci, or @clawsweeper address review.
  • Maintainers can comment @clawsweeper explain to ask for more context, or @clawsweeper stop to stop active automation.

History

Review history (5 earlier review cycles)
  • reviewed 2026-08-07T18:58:31.413Z sha 653816c :: needs real behavior proof before merge. :: [P2] Preserve the unreadable-source terminal result
  • reviewed 2026-08-07T19:14:04.254Z sha c9775a2 :: needs real behavior proof before merge. :: none
  • reviewed 2026-08-07T19:40:35.567Z sha c9775a2 :: needs real behavior proof before merge. :: none
  • reviewed 2026-08-07T21:23:17.374Z sha c9775a2 :: needs real behavior proof before merge. :: none
  • reviewed 2026-08-07T22:20:24.477Z sha c9775a2 :: needs real behavior proof before merge. :: none

A profile whose keychain CodexBar cannot read produces an indeterminate
observation and also satisfies isRefreshResultUnreadable. The indeterminate
branch was evaluated first, so it dropped isUnreadableAfterRefresh, replaced
the switch-source guidance with a generic retry message, and put the CLI back
on the short-cooldown relaunch loop removed in steipete#2650.

Order the terminal verdict first and cover it with a regression test.
make check flagged swiftTestingTestCaseNames and redundantSwiftTestingSuite.
Use backticked sentence names and drop the argument-less @suite, per AGENTS.md
and the surrounding suites.
@luisgonzaleznf

Copy link
Copy Markdown
Author

Both rank-up moves addressed where I can; being straight about the one I can't.

1. Restore the terminal unreadable-source handling + regression test — done in 695f478.

You and Codex independently found the same defect, and it was real. isRefreshResultUnreadable is true exactly when the keychain is unreadable and no credentials file exists — which is also exactly when the fingerprint probe returns nil and the observation is .indeterminate. My branch shadowed that path in the only state it exists to catch.

It cost two things, not one: isUnreadableAfterRefresh was dropped (so delegatedRefreshFailureMessage fell through to generic retry guidance instead of "Switch Claude Usage source to Web/CLI"), and it took the 20s cooldown instead of 5m, restoring the CLI relaunch loop #2650 removed. Ordering is now unreadabletouchError.indeterminate.unchanged.

Test added: unreadable refresh result outranks an unobservable keychain (nil fingerprint + no credentials file). I verified it fails against the pre-fix ordering — isUnreadableAfterRefresh: false and the wrong message string — rather than assuming it would.

Acceptance criteria: swift test --filter ClaudeOAuthDelegatedRefresh → 29/29. make check → 0 violations across 1806 files. make check also caught two naming violations in my new test file (swiftTestingTestCaseNames, redundantSwiftTestingSuite), fixed in c9775a2.

2. Release-signed runtime trace — I can't produce this, and I'd rather say so than dress up something weaker.

A local build is ad-hoc signed, so it cannot read CodexBar's own cache items:

Keychain preflight requires interaction for the current process (service=com.steipete.codexbar.cache)
Keychain cache item is unusable by this executable (oauth.claude.profile.<hash>); treating as missing

With no readable cache there is no expired-cached-credential precondition to recover from — the build lands in a different state ("credentials not found"), so any trace I captured would be evidence about code signing, not about this fix. Producing that proof requires the release signing identity, which only the maintainer has.

What I can offer instead: the pre-fix trace is from 0.48.0, Developer ID signed (my machine auto-updated mid-investigation), showing the bug is live on current release rather than only on the 0.47.0 I first reported — expiresInSec=-158241, hasRefreshToken=true, source=cacheKeychain, delegating every cycle and never recovering. status: 📣 needs proof looks like the correct call to me; I'd rather it sit there than have the gap papered over.

Happy to add anything else that's actually obtainable without the signing identity.

@luisgonzaleznf

Copy link
Copy Markdown
Author

@codex review

@clawsweeper re-review

Both review findings are addressed as of c9775a2 (ordering fix + regression test in 695f478, lint naming in c9775a2). swift test --filter ClaudeOAuthDelegatedRefresh → 29/29, make check → 0 violations.

@clawsweeper

clawsweeper Bot commented Aug 7, 2026

Copy link
Copy Markdown

🦞🧹
ClawSweeper re-review requested.

I asked ClawSweeper to review this item again.
Action: item re-review queued (workflow sweep.yml, event repository_dispatch).
Result: when the review finishes, ClawSweeper will create the durable review comment if needed or update the existing comment in place.

Re-review progress:

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Breezy!

Reviewed commit: c9775a223e

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

@luisgonzaleznf luisgonzaleznf changed the title Claude: recover an expired OAuth credential without a manual refresh Claude: adopt a valid credential when the delegated-refresh touch changed nothing Aug 7, 2026
@luisgonzaleznf

Copy link
Copy Markdown
Author

Correcting my own claim — this PR was overstated

An independent review pass caught something both bots and I missed, and it invalidates the headline claim I opened with. Retitled and rewrote the body; flagging it here rather than editing quietly.

What I claimed: an expired Claude OAuth credential can never recover on its own, and this restores background recovery.

What's actually true: under the default prompt policy this PR does not restore background recovery, because the code never reaches my change.

With claudeOAuthKeychainPromptMode at its default .onlyOnUserAction, a background refresh throws at ClaudeUsageFetcher.swift:404:

if policy.mode == .onlyOnUserAction,
   policy.interaction != .userInitiated,
   !allowBackgroundDelegatedRefresh          // hardcoded false, ClaudeProviderDescriptor.swift:676
{
    throw ClaudeUsageError.oauthFailed(
        "Claude OAuth token expired, but background repair is suppressed when Keychain prompt policy "
            + "is set to only prompt on user action. Click Refresh in the CodexBar menu to retry.")
}

That is thrown at :404. My change is at :440. It is never reached — and that thrown string is verbatim the error in the screenshot on #2733, so it is the actual cause of the reported symptom, not the && I fixed.

It wouldn't help to simply move the sync earlier, either: syncFromClaudeKeychainWithoutPrompt self-gates on the same policy via shouldAllowClaudeCodeKeychainAccess, which returns ProviderInteractionContext.current == .userInitiated under .onlyOnUserAction.

What the PR does still fix: the && short-circuit is a real bug. When the Claude CLI's token is already valid the touch legitimately changes nothing, the outcome is .attemptedFailed, and the sync that would adopt the valid credential is skipped. That matters for .always users (background recovery genuinely works) and for the post-delegation retry path generally. It just isn't the fix for the default-mode symptom.

Two smaller corrections in the same spirit: "runs the sync on every outcome" was overstated — in default Auto mode three outcomes still throw at :419-427 first; and my "0.48.0 still reproduces" logs show the stuck-expired symptom on current release, not the mechanism, which is reasoning from code paths.

On the real fix: making the default configuration self-heal means allowing a provably-non-prompting read to run under .onlyOnUserAction. That's a policy call about the Keychain surface, and I don't think a drive-by PR should make it — I tried relaxing an adjacent prompt gate earlier in this branch and it caused 8 test regressions. Happy to attempt it with direction, or to leave it.

Also flagging a gap I should have closed myself: 508c9f3 has no direct test. Restoring the && would still pass the suite.

Take this PR as the narrow fix it is, or close it — both are reasonable. I'd rather hand you an accurate small change plus the real diagnosis than an impressive-sounding wrong one.

@clawsweeper clawsweeper Bot added rating: 🧂 unranked krab Not merge-ready due to missing proof or serious correctness/safety concerns. and removed rating: 🦪 silver shellfish Thin PR readiness signal; proof, validation, or implementation needs work. labels Aug 7, 2026
@luisgonzaleznf

Copy link
Copy Markdown
Author

Closing — this change is a no-op in production

Closing my own PR. The fix cannot execute in a release build, and I'd rather say that plainly than leave it open for someone else to work out.

The chain:

  1. syncFromClaudeKeychainWithoutPrompt guards on shouldAllowClaudeCodeKeychainAccess (ClaudeOAuthCredentials.swift:1217)
  2. whose first line is guard self.keychainAccessAllowed else { return false }
  3. and keychainAccessAllowed is hardcoded false outside #if DEBUG (:2855-2871):
// Claude Code owns `Claude Code-credentials` and rewrites the item during token refreshes. That rewrite
// replaces its ACL, so any permission granted to CodexBar is inherently temporary and causes recurring
// macOS password dialogs. Production CodexBar therefore never reads the foreign item, with or without UI.
return false

So syncFromClaudeKeychainWithoutPrompt always returns false in production. Removing the && delegatedOutcome == .attemptedSucceeded short-circuits a call that returns false regardless — no behavioral change. The && isn't a bug; it's a redundant guard in front of an intentionally disabled path.

I should have found keychainAccessAllowed before opening this. My apologies for the review cycles — the two review passes here were correct and useful, and the ordering defect Codex and ClawSweeper both caught in 695f478 was real, but it was a real defect in a change that shouldn't have existed.

What survives, and is worth keeping: the diagnosis in #2733. Under the default prompt policy an expired OAuth credential has no recovery path, and the message the user sees blames the Keychain prompt policy rather than saying "this profile can't use the OAuth source — switch to Web/CLI". The terminal isUnreadableAfterRefresh path already produces exactly that better guidance; the default-mode gate at ClaudeUsageFetcher.swift:253-260 throws before it is ever reached. Reordering so users land on the actionable message instead of the misleading one looks like a small, self-contained improvement — happy to open that as a separate PR if it'd be welcome.

Also worth a mention independent of any of this: a no-UI kSecReturnData read of Claude Code-credentials measures 4–5.5s consistently on my machine versus 14ms for the attributes-only probe. Not relevant to this PR now, but it may be part of why manual refresh feels slow.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

merge-risk: 🚨 auth-provider 🚨 Merging this PR could break OAuth, tokens, provider routing, model choice, or credentials. P2 Normal priority bug or improvement with limited blast radius. rating: 🧂 unranked krab Not merge-ready due to missing proof or serious correctness/safety concerns. status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant