Skip to content

Claude: report the terminal verdict when no refresh can restore the profile - #2747

Closed
luisgonzaleznf wants to merge 1 commit into
steipete:mainfrom
luisgonzaleznf:fix/claude-unrecoverable-oauth-guidance
Closed

Claude: report the terminal verdict when no refresh can restore the profile#2747
luisgonzaleznf wants to merge 1 commit into
steipete:mainfrom
luisgonzaleznf:fix/claude-unrecoverable-oauth-guidance

Conversation

@luisgonzaleznf

Copy link
Copy Markdown

Fixes the user-facing half of #2733: a Claude profile that no refresh can restore is told to click Refresh, which sends the user round a loop that always lands back in the same place.

The loop

CodexBar already has the right message for this state — delegatedRefreshFailureMessage returns "Claude OAuth credentials expired and CodexBar cannot read them back… Switch Claude Usage source to Web/CLI." But under the default prompt policy the user rarely sees it, and never keeps it:

  1. A background cycle throws at ClaudeUsageFetcher.swift:404 (assertDelegatedRefreshAllowedInCurrentInteraction, with allowBackgroundDelegatedRefresh hardcoded false at ClaudeProviderDescriptor.swift:676) with "background repair is suppressed… Click Refresh in the CodexBar menu to retry."
  2. The user clicks Refresh. That interaction is .userInitiated, so delegation runs and reaches the accurate terminal verdict.
  3. The next background poll throws the retry message again, overwriting it.

So the actionable guidance is transient and the misleading guidance is the steady state. Clicking Refresh cannot fix this profile — there is nothing for a refresh to hand back.

Why the profile is unrecoverable

keychainAccessAllowed is hardcoded false outside #if DEBUG (ClaudeOAuthCredentials.swift:2855-2871) — production never reads Claude Code's Keychain item, deliberately, because Claude Code rewrites that item on every refresh and resets its ACL. With no ~/.claude/.credentials.json either, a delegated refresh has no channel to return a credential through.

That is precisely what isRefreshResultUnreadable already detects — but only after the touch, which background cycles never reach.

The change

When the prompt-policy branch is about to suppress background delegation, and the profile is provably unreadable, throw the terminal message instead of the retry suggestion.

Deliberately message-only:

  • Delegation behavior is unchanged. Background still skips it; .userInitiated still runs it. That matters because the coordinator notes "on older Claude Code a retried touch can still create the credentials file" — so the terminal verdict must not pre-empt the touch. It doesn't; this only picks the message on the path where delegation was already being skipped.
  • No prompt policy or Keychain gate is relaxed. Nothing about when CodexBar reads or prompts changes.
  • unreadableCredentialsMessage is extracted from delegatedRefreshFailureMessage and shared, so both paths stay in sync by construction.

Testing

  • New ClaudeUnrecoverableOAuthGuidanceTests — 3 cases: unrecoverable profile gets the terminal message, recoverable profile keeps the retry message, user-initiated is never suppressed.
  • Verified the key test fails without the fix, producing exactly the reported symptom: "…Click Refresh in the CodexBar menu to retry." where the terminal message is expected.
  • New appOAuthBackgroundReportsUnrecoverableProfileInsteadOfSuggestingRefresh covering the same via the full fetcher path.
  • make check → 0 violations across 1806 files.
  • No regressions vs main at 22b24b8. Remaining failures under a wide filter are pre-existing order-dependent suites (ClaudeOAuthPromptCoalescingTests, ClaudeCLISessionTests) — they fail on main too, the set shifts between runs of the same commit, and both pass 12/12 twice in isolation.

Two existing tests changed, and I want to flag why rather than bury it. appOAuthBackgroundRespectsPlatformKeychainPromptPolicy and oauth delegated retry only on user action background suppresses delegation both assert the retry wording while using environment: [:] with no credentials-file override. My change makes that message depend on whether a credentials file exists, so as written they would have become host-dependent. Both now pin a credentials file explicitly and keep their original assertions unchanged — the retry wording is still correct for a profile that is recoverable. Their behavioral assertions (attempts == 0, delegatedCounter == 0) are untouched.

Context

Reported from a real stuck profile: credential ~44h expired, hasRefreshToken=true, owner=claudeCLI, source=cacheKeychain, delegating every cycle and never recovering, on Developer ID signed 0.48.0 while claude itself worked throughout. The user spent a day on it because the message pointed at a Keychain setting; the fix was to stop using the OAuth source, which the app already knew and only said transiently.

This supersedes #2739, which I closed as a no-op — that one un-gated a sync that keychainAccessAllowed disables in production anyway. This change is on a path I've confirmed executes in production, which is what that one wasn't.

A profile with no Claude credentials file, whose Keychain item production
CodexBar never reads, cannot be restored by any refresh. Background cycles
still told the user to click Refresh: they click, the delegated path reaches
its terminal "cannot read them back / switch source" verdict, and the next
background poll overwrites that with the retry suggestion again.

Report the terminal verdict directly when the profile is provably unreadable.
Only the message changes: delegation is still skipped in background exactly as
before, and user-initiated refreshes still run it, so the older-Claude-Code
case where the touch itself creates the credentials file is untouched.

The two existing tests that assert the retry wording now pin their credentials
file explicitly. Without that the message they observe would depend on whether
the host running the tests happens to have one.

@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: 7d5bffa23c

ℹ️ 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".

/// production, so with no credentials file there is nothing a delegated refresh could hand back.
static func isDelegatedRefreshProvablyUnreadable(environment: [String: String]) -> Bool {
guard !ClaudeOAuthCredentialsStore.keychainAccessAllowed else { return false }
return !ClaudeOAuthCredentialsStore.hasSelectedProfileOAuthCredentialsFile(environment: environment)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve manual refresh guidance until after a touch

When a background poll hits onlyOnUserAction before the user has tried a manual delegated refresh, the absence of a credentials file is not enough to prove the profile is unrecoverable: the delegated-refresh coordinator intentionally treats older Claude Code as potentially able to create that file during a retried touch (ClaudeOAuthDelegatedRefreshCoordinator.swift:307-311). Returning true here makes those recoverable cached CLI credentials display the terminal “Switch source” message instead of the only action that can create the file, so this should depend on an actual post-touch isUnreadableAfterRefresh verdict or equivalent persisted evidence.

Useful? React with 👍 / 👎.

@clawsweeper clawsweeper Bot added 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. 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 8, 2026, 1:03 AM ET / 05:03 UTC.

ClawSweeper review

What this changes

The PR changes background Claude OAuth refresh guidance so profiles without a readable credentials file receive a switch-source message instead of a repeated Refresh suggestion.

Merge readiness

Blocked until real behavior proof is added - 6 items remain

Keep this PR open, but it is not ready to merge: its pre-touch file-absence heuristic can replace the only recovery action for older Claude Code profiles that create credentials during a manual refresh.

Priority: P2
Reviewed head: 7d5bffa23c2fc87081f1bb9dffbfc0d9d523b162

Review scores

Measure Result What it means
Overall readiness 🧂 unranked krab (1/6) A concrete recovery-path defect and missing real behavior proof keep the PR below merge-ready quality.
Proof confidence 🧂 unranked krab (1/6) Needs real behavior proof before merge: The body reports tests and checks but supplies no redacted after-fix run from an affected real setup or approved production-path trace. 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 🦪 silver shellfish (2/6) 1 actionable review finding remain.

Verification

Check Result Evidence
Real behavior Needs proof Needs real behavior proof before merge: The body reports tests and checks but supplies no redacted after-fix run from an affected real setup or approved production-path trace. 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 and released behavior: Current main and v0.48.1 retain the background-only prompt-policy failure that tells the user to click Refresh; the proposed guidance change is not already implemented.
Pre-touch classification in the PR: The proposed branch returns terminal guidance from the suppressed-background branch based only on disabled Keychain access and the current absence of a valid credentials file, before any delegated touch occurs.
Existing terminal contract: The coordinator defines an unreadable terminal result only after a clean delegated touch remains unobservable, explicitly preserving retryability because older Claude Code can create the credentials file during a retried touch.
Findings 1 actionable finding [P2] Defer terminal guidance until a delegated touch proves it
Security None None.

How this fits together

CodexBar polls Claude usage and surfaces OAuth recovery guidance in the menu bar. The refresh path first applies prompt policy, then may ask Claude Code to refresh credentials before returning usage or a user-facing failure message.

flowchart LR
A[Claude usage poll] --> B[Prompt policy]
B --> C{Background delegation allowed?}
C -->|yes| D[Claude Code refresh touch]
C -->|no| E[Background guidance]
D --> F[Observed credential result]
F --> G[Menu bar status]
E --> G
Loading

Before merge

  • Add real behavior proof - Needs real behavior proof before merge: The body reports tests and checks but supplies no redacted after-fix run from an affected real setup or approved production-path trace. 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.
  • Defer terminal guidance until a delegated touch proves it (P2) - This branch returns the terminal message before any delegated touch. That contradicts the coordinator’s existing contract that an older Claude Code touch can create the credentials file; a recoverable profile will be told to switch sources instead of taking the manual Refresh action that can repair it. Preserve the retry guidance until an actual clean touch records isUnreadableAfterRefresh (and retain that per-profile verdict for later background polls).
  • Resolve merge risk (P1) - Existing older-Claude-Code profiles whose credentials file is created only by a manual touch could be told to abandon Refresh even though that action can still recover the profile.
  • Complete next step (P2) - The P2 defect has a concrete code-and-test repair path, though real behavior proof remains a contributor-owned merge gate.
  • Improve patch quality - Change the terminal predicate to use a completed post-touch verdict and add coverage for the older-Claude-Code file-creation path.
  • Improve patch quality - Provide redacted after-fix evidence from the affected release-signed profile or an approved production-boundary trace.

Findings

  • [P2] Defer terminal guidance until a delegated touch proves it — Sources/CodexBarCore/Providers/Claude/ClaudeUsageFetcher.swift:257-260
Agent review details

Security

None.

Review metrics

Metric Value Why it matters
Production versus test delta production +28/-9; tests +177/-25 The patch adds substantial focused coverage, but the production predicate changes a provider recovery boundary.

Merge-risk options

Maintainer options:

  1. Use post-touch evidence for terminal guidance (recommended)
    Replace the file-absence heuristic with a persisted per-profile verdict from a completed clean delegated touch, then prove both the recoverable and terminal paths.

Technical review

Best possible solution:

Preserve Refresh guidance until a per-profile, completed delegated touch has recorded the coordinator’s actual unreadable verdict; only then retain terminal switch-source guidance across later background polls.

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

Yes from source: the PR’s background branch classifies a missing file before a delegated touch, while the coordinator documents that the touch can create that file on older Claude Code.

Is this the best way to solve the issue?

No. Terminal guidance should depend on the coordinator’s completed post-touch unreadable result, not on a pre-touch absence check.

Full review comments:

  • [P2] Defer terminal guidance until a delegated touch proves it — Sources/CodexBarCore/Providers/Claude/ClaudeUsageFetcher.swift:257-260
    This branch returns the terminal message before any delegated touch. That contradicts the coordinator’s existing contract that an older Claude Code touch can create the credentials file; a recoverable profile will be told to switch sources instead of taking the manual Refresh action that can repair it. Preserve the retry guidance until an actual clean touch records isUnreadableAfterRefresh (and retain that per-profile verdict for later background polls).
    Confidence: 0.99

Overall correctness: patch is incorrect
Overall confidence: 0.98

AGENTS.md: found and applied where relevant.

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

Labels

Label justifications:

  • P2: The defect affects a bounded Claude OAuth recovery path and can misdirect users with recoverable profiles.
  • merge-risk: 🚨 auth-provider: Merging changes OAuth recovery guidance for existing Claude credential profiles before the refresh touch can establish its outcome.
  • rating: 🧂 unranked krab: Overall readiness is 🧂 unranked krab; proof is 🧂 unranked krab and patch quality is 🦪 silver shellfish.
  • status: 📣 needs proof: The PR needs real behavior proof before ClawSweeper can clear the contributor ask. Needs real behavior proof before merge: The body reports tests and checks but supplies no redacted after-fix run from an affected real setup or approved production-path trace. 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

Acceptance criteria:

  • [P1] swift test --filter ClaudeUnrecoverableOAuthGuidanceTests.
  • [P1] swift test --filter ClaudeOAuthDelegatedRefreshUnreadableResultTests.
  • [P1] make test.
  • [P1] make check.

What I checked:

Likely related people:

  • Peter Steinberger: Current delegated-refresh coordinator behavior and its post-touch terminal-result contract trace to this recent provider refactor. (role: recent area contributor; confidence: high; commits: 973ec704de4d; files: Sources/CodexBarCore/Providers/Claude/ClaudeOAuth/ClaudeOAuthDelegatedRefreshCoordinator.swift)
  • Yuxin Qiao: Introduced the stored prompt-policy and background delegated-refresh guards that this PR modifies. (role: introduced background prompt-policy behavior; confidence: high; commits: a27c9b094e61, 47120d4a03b4, d686b4bdbbd7; files: Sources/CodexBarCore/Providers/Claude/ClaudeUsageFetcher.swift)

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 (2 earlier review cycles)
  • reviewed 2026-08-07T23:52:10.524Z sha 7d5bffa :: needs real behavior proof before merge. :: [P2] Do not classify the profile before the first delegated touch
  • reviewed 2026-08-08T00:31:34.586Z sha 7d5bffa :: needs real behavior proof before merge. :: [P2] Defer terminal guidance until after a delegated touch

@steipete

steipete commented Aug 8, 2026

Copy link
Copy Markdown
Owner

Thanks for digging into this — the stuck-card loop you're describing is real, but this implementation can't land in its current shape, and the reasoning is worth spelling out.

The patch computes "provably unreadable" before the delegated touch (Keychain unavailable + credentials file absent) and reports terminal guidance from that. That widens terminal classification in exactly the way #2650's review deliberately rejected: on older Claude Code versions, the touch itself can create ~/.claude/.credentials.json — which is why the coordinator re-checks the file after the touch and keeps touch errors retryable (there's a regression test locking that invariant). Pre-touch absence is not proof that no refresh can restore the profile, and a background poll here can mask a retryable touch error with permanent-sounding guidance.

The underlying experience you're fixing is the target of #2675 (consented direct read + CLI usage fallback), which resolves the stuck card without weakening the classifier. Once that lands, if persistent terminal guidance across background polls is still desirable, the right construction is to persist the actual post-touch terminal verdict and surface that — happy to review a follow-up in that shape. Closing this one in the meantime rather than leaving it in limbo; the analysis in your PR body genuinely helped validate #2675's direction.

@steipete steipete closed this Aug 8, 2026
@luisgonzaleznf

Copy link
Copy Markdown
Author

Understood, and the Codex finding was right — thanks for the review time.

My mistake was reasoning about the code path and not the user path. Background still skipped delegation exactly as before, so I convinced myself the older-Claude-Code touch was preserved. But dropping "Click Refresh" from the message removes the prompt to perform that touch, so a recoverable profile could be told to abandon the one action that would have fixed it. Worse than the bug I was aiming at.

A corrected version would gate on an actual post-touch isUnreadableAfterRefresh verdict, persisted per profile so background polls can reuse it, rather than the pre-touch heuristic — terminal guidance only after a real touch has proven it.

Worth me putting that together, or would you rather leave this alone? Happy either way — no reply needed if it's a no, I won't follow up.

The observed-behaviour writeup in #2733 stands on its own regardless.

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.

2 participants