Skip to content

Claude: stop rotating CLI-owned refresh chains on keychain-only installs - #2745

Merged
steipete merged 6 commits into
steipete:mainfrom
avenoxai:fix/claude-oauth-refresh-chain-ownership
Aug 9, 2026
Merged

Claude: stop rotating CLI-owned refresh chains on keychain-only installs#2745
steipete merged 6 commits into
steipete:mainfrom
avenoxai:fix/claude-oauth-refresh-chain-ownership

Conversation

@avenoxai

@avenoxai avenoxai commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Summary

Two surgical fixes in the Claude OAuth credential-ownership seam:

  1. Never hijack a live Claude CLI refresh chain. resolvedCacheOwner downgrades a .codexbar-owned cache entry to .claudeCLI (delegated refresh) unless there is positive evidence that Claude CLI storage is gone. Previously, "cannot see CLI storage" was treated as "CLI storage absent" — on keychain-only Claude Code installs (2.1.x, no ~/.claude/.credentials.json, and keychainAccessAllowed == false in release since e17ba24bd), that evidence could never be produced, so a persisted .codexbar entry stayed CodexBar-owned forever.
  2. Scope the terminal invalid_grant block to the token lineage that failed. The refresh-failure gate now records a SHA-256 hash of the refresh token that produced invalid_grant, and a refresh attempt with a different refresh token is allowed through (re-latching with the new hash if it also fails). Previously the block could only clear via a file-fingerprint change — impossible on keychain-only installs, where the fingerprint is permanently nil, so "blocked until auth changes. Run claude to re-authenticate." was unsatisfiable: re-auth could never clear it.

Root cause

Anthropic refresh tokens are single-use and rotating. When an expired cache entry stays .codexbar-owned, CodexBar POSTs the refresh itself, accepts the rotated refresh token, and stores it only in its own cache — Claude Code's stored token is now server-side invalid, so Claude Code's next refresh gets invalid_grant and the user is logged out of Claude Code (the #1161 failure class). The self-renewing mirror also never expires, so it permanently shadows the real account state — the "CodexBar keeps reverting to my old Claude account" symptom (#2689).

The decision helper hasClaudeCLIStorageWithoutPrompt only accepted two proofs of CLI ownership: a credentials-file fingerprint, or a .matched no-prompt keychain probe. On keychain-only installs in release, both are structurally impossible — the probe API already distinguishes .unavailable (can't look) from .absent (looked, nothing there), but the caller collapsed them.

What changed

hasClaudeCLIStorageWithoutPrompt now implements an explicit decision table:

Evidence Verdict
Credentials-file fingerprint present CLI owns (unchanged)
Probe .matched CLI owns (unchanged)
Probe .mismatch (item exists, different token) CLI owns (new — item existence alone proves CLI storage)
Probe .absent (readable, no item) CodexBar may keep its mirror (unchanged outcome, now explicit)
Probe .unavailable / prompt mode Never New: fall back to the prompt-free ClaudeAccountProfile.accountUuid read of Claude's plaintext config — a logged-in Claude Code install means the CLI owns the lifecycle

The only scenario that keeps direct refresh alive is the one it was built for: Claude Code is genuinely gone or logged out, and CodexBar keeps its own mirrored chain running — where rotation can't hurt anyone.

The failure gate keeps its public API (new parameters are defaulted), stays monotonic, and persists the failed-lineage hash per profile alongside the existing keys. Raw tokens are never persisted or logged — hash only.

Composition with in-flight PRs

Verification

  • swift test --filter ClaudeOAuthRefreshChainOwnershipTests — 6 new decision-table/delegation tests (probe .unavailable × config present/absent, .absent, .mismatch, file fingerprint, prompt-mode Never fallback), all passing.
  • swift test --filter ClaudeOAuthRefreshFailureGateTests — 3 new lineage tests (scoped block, legacy-block healing + re-latch, persistence round-trip), 16/16 passing.
  • swift test --filter ClaudeOAuthCredentialsStoreCLIStorageOwnershipTests — 9/9 (one assertion updated: .mismatch now proves CLI storage; fixtures isolated from the developer's ambient Claude config via CLAUDE_CONFIG_DIR).
  • swift test --filter ClaudeUsageTests (40/40) and --filter ClaudeOAuthFetchStrategyAvailabilityTests (22/22).
  • make check — SwiftFormat clean, SwiftLint strict zero violations.
  • make test — full sharded suite green except one pre-existing, environment-caused failure: MiniMaxMenuCardBillingTests."minimax billing history renders inline dashboard" expects "1,234" but gets "1.234" on a Turkish-locale machine (thousands-separator hardcoded). It fails identically on an unmodified origin/main worktree (both-trees solo runs), so it is unrelated to this change and should be green on CI's en-US locale.
  • One pre-existing test updated for the new semantics: load with auto refresh expired codexbar owner uses direct refresh path now pins its CLAUDE_CONFIG_DIR to an empty temp dir — it covers the CLI-absent path, and without isolation the developer's own logged-in ~/.claude.json would (correctly, under the new rule) hand the chain to the CLI.
  • Per AGENTS.md, no prompt-capable validation was run: all coverage uses test stores, task-local overrides, and temp-directory CLAUDE_CONFIG_DIR fixtures; no real SecItem reads, no live probes, no packaged-app launches.

Fixes #2689. Refs #2634 (the unrecoverable-after-re-auth tail), #1161 (the original rotation-desync report), #2115/#2195 (prompt-loop context).

@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: 49ff8c9d97

ℹ️ About Codex in GitHub

Your team has set up Codex to 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 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +756 to +757
case .unavailable, .notApplicable:
return ClaudeAccountProfile.accountUuid(environment: environment) != nil

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Treat unreadable profile config as unknown ownership

When the Keychain probe is unavailable—which is the normal release configuration—accountUuid returns nil not only when the CLI is logged out, but also when its config is unreadable, malformed, or uses an unrecognized schema. In those cases this marks the cached chain as CodexBar-owned, so an expired mirror is refreshed directly and its rotating token can invalidate the still-live Claude CLI chain. Preserve CLI ownership unless the plaintext config can positively establish that CLI storage is absent.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 3ce97e6: ownership evidence is now tri-state (signedIn / signedOut / indeterminate). Only a verifiably missing config or a cleanly parsed signed-out config releases the chain to CodexBar; unreadable, malformed, unrecognized-schema, and identity-less configs are indeterminate and stay CLI-owned. Regression tests cover all three indeterminate shapes.

@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. P1 Urgent regression or broken agent/channel workflow affecting real users now. 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, 9:15 PM ET / August 9, 2026, 01:15 UTC.

ClawSweeper review

What this changes

The branch conservatively delegates Claude OAuth refresh to the CLI when CLI ownership cannot be disproved, and ties terminal refresh blocks to the refresh-token lineage that failed.

Regression provenance

Possible regression — suspected (reviewed change). No predecessor PR is attributed.

Merge readiness

Blocked until real behavior proof is added - 3 items remain

The final source addresses the prior ownership and lineage findings, but this active, maintainer-engaged PR still needs real after-fix proof on the release-signed keychain-only path before merge.

Priority: P1
Reviewed head: 6e5af9894ec960f6e67aa73d0cf0af6fdb2afb9e

Review scores

Measure Result What it means
Overall readiness 🧂 unranked krab (1/6) The reviewed implementation and regression coverage are solid, but the required real behavior proof is still absent.
Proof confidence 🧂 unranked krab (1/6) Needs real behavior proof before merge: Focused tests are supplemental; the PR lacks a redacted after-fix trace from a release-signed, keychain-only Claude installation showing the changed ownership and recovery behavior. 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 🐚 platinum hermit (4/6) No actionable review findings were identified.

Verification

Check Result Evidence
Real behavior Needs proof Needs real behavior proof before merge: Focused tests are supplemental; the PR lacks a redacted after-fix trace from a release-signed, keychain-only Claude installation showing the changed ownership and recovery behavior. 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 7 items Current-main gap: Current main lacks the tri-state config evidence and token-lineage handling introduced by this branch, so the central behavior is not already implemented.
Conservative ownership implementation: The final head treats unavailable or indeterminate Keychain/config evidence as CLI-owned; only positively signed-out config releases a CodexBar cache chain for direct refresh.
Lineage gate integration: The direct refresh path hashes the attempted refresh token and supplies that hash to both admission and failure recording, avoiding a global terminal block after credentials change.
Findings None None.
Security None None.

How this fits together

CodexBar’s Claude provider decides whether a cached OAuth credential may be refreshed directly or must be handed to Claude CLI. That ownership decision feeds usage refresh and protects rotating CLI refresh tokens from being consumed by CodexBar.

flowchart LR
    A[Cached Claude credentials] --> B[Ownership evidence]
    C[Claude config and Keychain state] --> B
    B --> D{Refresh owner}
    D -->|Claude CLI| E[Delegated CLI refresh]
    D -->|CodexBar only| F[Direct OAuth refresh]
    E --> G[Usage update]
    F --> G
Loading

Before merge

  • Add real behavior proof - Needs real behavior proof before merge: Focused tests are supplemental; the PR lacks a redacted after-fix trace from a release-signed, keychain-only Claude installation showing the changed ownership and recovery behavior. 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.
  • Resolve merge risk (P2) - The conservative fallback changes existing cache-only profiles to delegated refresh whenever CLI config is indeterminate; a release-signed keychain-only trace must show this prevents rotation without making legitimate cache-only recovery unavailable.
  • Complete next step (P2) - This active PR needs contributor-supplied real behavior proof, not an automated repair or backlog-cleanup action.
Agent review details

Security

None.

Review metrics

Metric Value Why it matters
Production versus test delta production +135/-26, tests +477/-12 The sizeable focused test addition covers the new ownership decision table and persisted token-lineage transitions.

Root-cause cluster

Relationship: fixed_by_candidate
Canonical: #2689
Summary: This PR is the active candidate fix for the reported Claude account reversion behavior.

Members:

Proposal only: this assessment does not dispatch repair, suppress jobs, mutate sibling items, close, or merge anything.

Merge-risk options

Maintainer options:

  1. Prove the release-signed ownership path (recommended)
    Capture a redacted after-fix trace on a keychain-only Claude CLI installation showing delegated ownership and recovery without rotating the CLI chain.

Technical review

Best possible solution:

Land the conservative ownership and lineage behavior only after a redacted release-signed keychain-only trace demonstrates delegated ownership and successful post-reauth recovery without rotating the Claude CLI chain.

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

No live reproduction was established in this review; the current source and focused tests provide a high-confidence source reproduction of the affected ownership and gate paths.

Is this the best way to solve the issue?

Yes for the code direction: retaining CLI ownership unless absence is positively proven is the narrowest safe response to rotating refresh tokens, pending real release-signed proof.

AGENTS.md: found and applied where relevant.

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

Labels

Label justifications:

  • P1: The PR targets an OAuth refresh-chain defect that can log users out of Claude Code or leave Claude usage unrecoverable.
  • merge-risk: 🚨 compatibility: The ownership decision changes whether existing cached credentials use direct or delegated refresh after upgrade.
  • merge-risk: 🚨 auth-provider: The patch changes Claude OAuth token refresh routing and terminal failure recovery.
  • rating: 🧂 unranked krab: Overall readiness is 🧂 unranked krab; proof is 🧂 unranked krab and patch quality is 🐚 platinum hermit.
  • status: 📣 needs proof: The PR needs real behavior proof before ClawSweeper can clear the contributor ask. Needs real behavior proof before merge: Focused tests are supplemental; the PR lacks a redacted after-fix trace from a release-signed, keychain-only Claude installation showing the changed ownership and recovery behavior. 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:

  • steipete: The repository owner reviewed the ownership design, specified the two prior required fixes, and merged recent main changes into the branch. (role: reviewer and recent area contributor; confidence: high; commits: 6e5af9894ec9, 5e8797b14b6c; files: Sources/CodexBarCore/Providers/Claude/ClaudeOAuth/ClaudeOAuthCredentials.swift, Sources/CodexBarCore/Providers/Claude/ClaudeOAuth/ClaudeOAuthRefreshFailureGate.swift)
  • ratulsarna: Authored the original terminal invalid_grant and backoff behavior that this PR extends with token-lineage state. (role: original refresh-gate author; confidence: high; commits: 85090e870b75; files: Sources/CodexBarCore/Providers/Claude/ClaudeOAuth/ClaudeOAuthRefreshFailureGate.swift)
  • Cameron Beeley: Authored the recent task-local fingerprint override refactor in the same persisted refresh-failure gate. (role: recent gate refactor author; confidence: medium; commits: bd23e637771f; files: Sources/CodexBarCore/Providers/Claude/ClaudeOAuth/ClaudeOAuthRefreshFailureGate.swift)

Rank-up moves

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

  • Add a redacted after-fix release-signed trace showing delegated ownership and recovery without Claude CLI logout.

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 (9 earlier review cycles; latest 8 shown)
  • reviewed 2026-08-08T00:22:33.128Z sha 49ff8c9 :: needs real behavior proof before merge. :: [P1] Treat unreadable Claude config as unknown ownership
  • reviewed 2026-08-08T05:04:16.888Z sha 49ff8c9 :: needs real behavior proof before merge. :: [P1] Preserve CLI ownership when profile config is unreadable
  • reviewed 2026-08-08T11:11:26.180Z sha 3ce97e6 :: needs real behavior proof before merge. :: none
  • reviewed 2026-08-08T11:26:02.350Z sha 3ce97e6 :: needs real behavior proof before merge. :: none
  • reviewed 2026-08-08T21:11:57.263Z sha 3a979ec :: needs real behavior proof before merge. :: none
  • reviewed 2026-08-08T21:50:47.999Z sha b8599a7 :: needs real behavior proof before merge. :: none
  • reviewed 2026-08-08T22:32:45.778Z sha b8599a7 :: needs real behavior proof before merge. :: none
  • reviewed 2026-08-08T23:41:52.077Z sha b8599a7 :: needs real behavior proof before merge. :: [P1] Fail closed for unrecognized Claude config

@steipete

steipete commented Aug 8, 2026

Copy link
Copy Markdown
Owner

Reviewed in depth — the ownership diagnosis is strong and this is a genuinely valuable find: on keychain-only installs the delegated touch rotates Claude Code's own refresh chain while CodexBar can never read the result, so every retry actively worsens the user's state. The design direction (stop rotating CLI-owned chains) is right, and this should land before #2675, which will rebase on top so its consent path composes with your ownership logic.

Two gaps to close before merge:

  1. Indeterminate config must be conservative. As written, an unreadable/ambiguous plaintext config can still select the direct-refresh path, bypassing both your protection and Report unreadable Claude OAuth refresh as terminal #2650's classifier. Ownership evidence needs to be tri-state (CLI-owned / CodexBar-owned / indeterminate), with indeterminate treated as CLI-owned — never rotating a chain we can't prove we own.
  2. Legacy-lineage transient backoff. The transition from a legacy lineage into transient backoff currently drops a state it should carry forward; needs the transition repaired plus a focused regression test for both gaps.

Then rebase onto current main (the subsystem moved this week: #2650 terminal-state handling and the #2675 branch both touch ClaudeOAuthCredentials.swift) and rerun swift test --filter ClaudeOAuth (the PromptCoalescing/ProfileIsolation parallel flakes are pre-existing baseline noise — both pass in isolation). With those addressed this is mergeable. Nice work.

@avenoxai
avenoxai force-pushed the fix/claude-oauth-refresh-chain-ownership branch from 49ff8c9 to 3ce97e6 Compare August 8, 2026 11:07
@avenoxai

avenoxai commented Aug 8, 2026

Copy link
Copy Markdown
Contributor Author

Both gaps closed, and rebased onto current main (the CHANGELOG entry moved to the 0.48.2 unreleased section).

1. Indeterminate config evidence is now conservative. New tri-state ClaudeAccountProfile.configOwnershipEvidence: signedIn / signedOut / indeterminate. signedOut requires positive evidence — the config file verifiably absent (no-such-file on read), or a cleanly parsed config with no oauthAccount. An unreadable config (permissions/I-O error), a malformed or unrecognized schema, and an oauthAccount stanza without a usable accountUuid are all indeterminate → treated as CLI-owned, so a chain we can't prove we own is never rotated. accountUuid(environment:) now derives from the same evidence, so the two readers can't drift.

2. Superseded-lineage transient failures now transition into backoff. recordTransientFailure takes the attempt's lineage hash: a transient failure on a lineage different from the terminally-blocked one clears the dead lineage's terminal block and records normal transient backoff, instead of being dropped by the monotonic guard (which previously left the new lineage free to retry immediately with no cooldown). Same-lineage and hash-less callers keep the exact old monotonic behavior, and shouldAttempt's new-lineage allowance now honors an active transient backoff as well.

Regression tests — three config-evidence cases (malformed config, oauthAccount without identity, cleanly signed-out config) and three gate-transition cases (legacy nil-hash block → new-lineage backoff, old-lineage block yields to new-lineage backoff, same-lineage stays terminal).

Verification after the rebase: swift test --filter ClaudeOAuth — 282 tests in 33 suites; the only failures are the PromptCoalescing/ProfileIsolation parallel flakes you named, and both suites pass in isolation (5/5 and 2/2). make check clean.

One drive-by observation from triaging those flakes: when ClaudeOAuthDelegatedRefreshProfileIsolationTests."legacy cooldown migrates only to the default credentials profile" fails, its expectation message interpolates defaultEnvironment — the developer's entire process environment, secrets included — into the test log. Worth a small follow-up to keep that failure message redacted; happy to file an issue or fix it separately.

@steipete

steipete commented Aug 8, 2026

Copy link
Copy Markdown
Owner

Checking in — the two asks from review (tri-state ownership evidence with indeterminate-treated-as-CLI-owned, and the legacy-lineage transient-backoff transition, both with regression tests) are the only things between this and merge, and #2675 is queued to rebase on top of you. If you're short on time this week, say so and we'll carry the fixes over the line with your authorship preserved — the ownership diagnosis is yours either way.

@clawsweeper clawsweeper Bot added rating: 🧂 unranked krab Not merge-ready due to missing proof or serious correctness/safety concerns. merge-risk: 🚨 compatibility 🚨 Merging this PR could break existing users, config, migrations, defaults, or upgrades. and removed rating: 🦪 silver shellfish Thin PR readiness signal; proof, validation, or implementation needs work. labels Aug 8, 2026
@steipete
steipete merged commit 33f6ec4 into steipete:main Aug 9, 2026
16 of 18 checks passed
@steipete

steipete commented Aug 9, 2026

Copy link
Copy Markdown
Owner

Landed. Verification: the ownership diagnosis was confirmed against the delegated-refresh coordinator (on keychain-only installs each touch rotated Claude Code's refresh chain while CodexBar could never read the result — actively worsening state per retry). The two review gaps were carried over the line with your authorship preserved: tri-state ownership evidence (indeterminate → treated as CLI-owned, never rotating an unproven chain) and the legacy-lineage transient-backoff transition, both regression-tested (ownership 11/11, consent 10/10, CLI-storage 9/9). #2675 is rebased on top so the consent path composes with this. CI green at merge after the runner-flake rerun. Thanks @avenoxai — the mechanism find here was the missing piece of the 0.47 recovery story.

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. merge-risk: 🚨 compatibility 🚨 Merging this PR could break existing users, config, migrations, defaults, or upgrades. P1 Urgent regression or broken agent/channel workflow affecting real users now. 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.

CodexBar keeps reverting to old Claude account/email despite repeated sign-ins on new account/email

2 participants