Skip to content

feat(claude): read OAuth usage per CLAUDE_CONFIG_DIR account#1829

Closed
LawyZheng wants to merge 1 commit into
steipete:mainfrom
LawyZheng:feat/claude-config-dir-multi-account
Closed

feat(claude): read OAuth usage per CLAUDE_CONFIG_DIR account#1829
LawyZheng wants to merge 1 commit into
steipete:mainfrom
LawyZheng:feat/claude-config-dir-multi-account

Conversation

@LawyZheng

Copy link
Copy Markdown

Summary

Makes Claude OAuth credential resolution CLAUDE_CONFIG_DIR-aware so a single CodexBar process can read usage for multiple Claude subscription accounts — without an external dependency or manual token pasting.

Background

Claude Code isolates each CLAUDE_CONFIG_DIR into its own macOS Keychain entry and credentials file:

Config dir Keychain service File
~/.claude (default) Claude Code-credentials ~/.claude/.credentials.json
custom CLAUDE_CONFIG_DIR Claude Code-credentials-<hash> $CLAUDE_CONFIG_DIR/.credentials.json

<hash> is the first 8 hex chars of the config directory path's SHA-256. Verified against a real machine: sha256("/Users/…/.claude-work")[0..<8]da6d47a8 → Keychain entry Claude Code-credentials-da6d47a8.

Previously claudeKeychainService and the credentials file path were hardcoded to the default account, so CodexBar could only ever read one Claude subscription (#81, #1756). Because Claude Code creates a distinct Keychain entry per config dir, deriving the service name from the active CLAUDE_CONFIG_DIR is enough to read any account's usage.

What changed

  • Keychain serviceclaudeKeychainService becomes a computed property derived from the active CLAUDE_CONFIG_DIR. Making the single constant computed means all 14 existing references (candidate probes, preflight, the security find-generic-password -s reader, and log labels) resolve to the correct account with no other call-site changes.
  • Credentials filedefaultCredentialsURL() resolves $CLAUDE_CONFIG_DIR/.credentials.json for the file fallback.
  • Cache isolation — the in-memory and Keychain credential caches are scoped per account so two accounts never cross-read. The default account's cache key is unchanged (byte-for-byte backward compatible); KeychainCacheStore.Key.oauth gains an optional accountScope following the existing cookie(…scopeIdentifier:) pattern.
  • Injection — the config dir is injected as a TaskLocal at the loadRecord entry point (the environment dict already flows through the whole read path), so the entire synchronous read sees it.

How it enables multi-account

A caller reads a specific account's usage by supplying its CLAUDE_CONFIG_DIR in the fetch environment, e.g. ["CLAUDE_CONFIG_DIR": "~/.claude-work"]. The default (empty / ~/.claude) path is completely unchanged.

This PR is infrastructure only — it does not add UI, account enumeration, or credential storage. It's the credential-layer primitive any multi-account approach (menu items, claude-swap, etc.) needs.

Tests

ClaudeOAuthConfigDirAccountTests:

  • service-name derivation (default → suffix-less; custom → -<hash>; trailing-slash normalization; hash == sha256 first 8 hex)
  • reading a credentials file from a custom config dir
  • the in-memory cache not leaking tokens between two accounts

The hash/normalization logic was also cross-checked independently against the real Keychain entry on macOS.

Note: swift build is clean. I couldn't run swift test locally because a transitive KeyboardShortcuts SwiftUI #Preview macro fails to load under my local Swift 6.3 toolchain (unrelated to this change) — CI should run the suite normally.

Related

🤖 Generated with Claude Code

Claude Code isolates each `CLAUDE_CONFIG_DIR` into its own macOS Keychain
entry: `Claude Code-credentials` for the default `~/.claude`, and
`Claude Code-credentials-<hash>` for a custom dir, where `<hash>` is the
first 8 hex chars of the config directory path's SHA-256. Credentials also
live at `$CLAUDE_CONFIG_DIR/.credentials.json`.

Previously the service name and file path were hardcoded to the default
account, so CodexBar could only ever read one Claude subscription. This
makes credential resolution config-dir aware so a single CodexBar process
can read usage for multiple Claude accounts by supplying a per-account
`CLAUDE_CONFIG_DIR` in the fetch environment (the environment dictionary
already flows through the whole read path).

Changes:
- Derive the Keychain service name from the active `CLAUDE_CONFIG_DIR`
  (verified against a real entry: sha256(dir)[0..<8]). Turning the single
  `claudeKeychainService` constant into a computed property makes all 14
  existing references (queries, preflight, security-CLI reader, logs)
  resolve to the correct account with no other call-site changes.
- Resolve `$CLAUDE_CONFIG_DIR/.credentials.json` for the file fallback.
- Isolate the in-memory and Keychain credential caches per account so two
  accounts never cross-read; the default account's cache key is unchanged
  (backward compatible).
- Inject the config dir as a TaskLocal at the `loadRecord` entry point so
  the whole synchronous read path sees it.

Add ClaudeOAuthConfigDirAccountTests covering service-name derivation
(incl. default = suffix-less, trailing-slash normalization), reading a
credentials file from a custom config dir, and the memory cache not
leaking tokens between accounts.

This is infrastructure only: no UI, account enumeration, or credential
storage is added.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@clawsweeper

clawsweeper Bot commented Jul 1, 2026

Copy link
Copy Markdown

Codex review: needs real behavior proof before merge. Reviewed July 1, 2026, 3:30 PM ET / 19:30 UTC.

Summary
The branch makes Claude OAuth credential lookup derive Keychain service, credentials file, and cache keys from CLAUDE_CONFIG_DIR, with new tests for hashing, custom config files, and memory-cache isolation.

Reproducibility: yes. for the review finding: source inspection of the PR head shows CLAUDE_CONFIG_DIR is installed only for loadRecord, while cache/probe/sync helpers still evaluate account-derived state without that scope. Real macOS runtime proof is still missing.

Review metrics: 2 noteworthy metrics.

  • Changed auth surface: 3 source files changed, 1 test file added. The diff changes Claude OAuth credential resolution and cache identity, so the merge decision is auth/provider-sensitive rather than routine cleanup.
  • Runtime proof: 0 inspectable artifacts. The PR body reports a local Keychain check but provides no reviewable after-fix output for the real macOS credential path.

Merge readiness
Overall: 🧂 unranked krab
Proof: 🧂 unranked krab
Patch quality: 🧂 unranked krab
Result: blocked until real behavior proof is added.

Overall follows the weaker of proof and patch quality, so missing proof can cap an otherwise strong patch.

Rank-up moves:

  • [P1] Add redacted terminal/log proof showing after-fix custom CLAUDE_CONFIG_DIR credential resolution on macOS; updating the PR body should trigger a fresh ClawSweeper review, or a maintainer can comment @clawsweeper re-review.
  • Propagate account scope through cache probes, Keychain probes, invalidation, fingerprinting, and delegated-refresh sync, with focused regression tests for default and custom accounts.
  • Get maintainer agreement on whether native CLAUDE_CONFIG_DIR Keychain reads should complement the linked read-only claude-swap architecture.

Proof guidance:

  • [P1] Needs real behavior proof before merge: No inspectable after-fix real behavior proof was provided; a redacted terminal log, copied live output, or screenshot/recording showing a custom CLAUDE_CONFIG_DIR Keychain/file read would be needed, with private paths and tokens redacted. 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.

Risk before merge

  • [P1] Account scope is currently installed only for loadRecord, so cache probes and post-delegated-refresh sync can still consult the default Claude account while a caller requested a custom CLAUDE_CONFIG_DIR.
  • [P1] The linked owner discussion currently frames multi-account support as a product/auth decision and recommends a read-only claude-swap adapter boundary before native credential handling.
  • [P1] The PR lacks inspectable after-fix macOS proof for the Keychain/config-dir path; the prose says it was checked locally, but there is no redacted terminal output, log, screenshot, or recording to review.

Maintainer options:

  1. Fix account scoping before merge (recommended)
    Make the account scope part of the credential context for cache probes, Keychain probes, fingerprinting, invalidation, and delegated-refresh sync, then add focused tests for those paths.
  2. Decide native Keychain scope first
    Pause until maintainers choose whether this native CLAUDE_CONFIG_DIR credential primitive should supersede or complement the read-only claude-swap adapter direction.
  3. Accept the auth boundary explicitly
    Maintainers may accept the broader credential-reading surface intentionally, but the PR should say how custom account credentials are isolated from default account state during upgrades and refreshes.

Next step before merge

  • [P1] Maintainers need to decide the native credential-reading direction and the contributor needs to provide real macOS proof; the current blocker is not just a standalone automation repair.

Security
Needs attention: The diff changes Claude secret selection and has an account-scoping gap that can mix default and custom account credential state.

Review findings

  • [P1] Propagate account scope beyond loadRecord — Sources/CodexBarCore/Providers/Claude/ClaudeOAuth/ClaudeOAuthCredentials.swift:226-229
Review details

Best possible solution:

Either approve a native CLAUDE_CONFIG_DIR credential primitive and propagate account scope through every probe/cache/refresh path with real macOS proof, or keep the read-only claude-swap adapter as the first implementation path.

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

Yes for the review finding: source inspection of the PR head shows CLAUDE_CONFIG_DIR is installed only for loadRecord, while cache/probe/sync helpers still evaluate account-derived state without that scope. Real macOS runtime proof is still missing.

Is this the best way to solve the issue?

No; the implementation should make account scope an explicit credential-context concern across all credential probes and refresh paths, not a TaskLocal wrapper around only the main load path. Maintainers also need to choose whether native Keychain/config-dir reads fit the approved multi-account direction.

Full review comments:

  • [P1] Propagate account scope beyond loadRecord — Sources/CodexBarCore/Providers/Claude/ClaudeOAuth/ClaudeOAuthCredentials.swift:226-229
    CLAUDE_CONFIG_DIR is installed as a TaskLocal only for loadRecord, but other environment-aware paths such as hasCachedCredentials(environment:) and the post-delegated-refresh sync still evaluate cacheKey, defaultCredentialsURL, and claudeKeychainService without that scope. In a default-plus-custom setup, probes or silent refresh can read/write default-account state while the caller asked for the custom config dir, defeating the isolation this PR is adding.
    Confidence: 0.87

Overall correctness: patch is incorrect
Overall confidence: 0.82

AGENTS.md: found and applied where relevant.

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

Label changes

Label changes:

  • add P2: This is a normal-priority Claude auth-provider feature with limited blast radius, but it touches credential routing and has a blocking scoping defect before merge.
  • add merge-risk: 🚨 auth-provider: Merging could route Claude OAuth availability, cache, and delegated refresh through the wrong account when default and custom config-dir accounts coexist.
  • add merge-risk: 🚨 security-boundary: The diff broadens which Claude credentials CodexBar reads from files and Keychain services, so account isolation and maintainer-approved auth boundaries need proof before merge.
  • add rating: 🧂 unranked krab: Overall readiness is 🧂 unranked krab; proof is 🧂 unranked krab and patch quality is 🧂 unranked krab.
  • add status: 📣 needs proof: The PR needs real behavior proof before ClawSweeper can clear the contributor ask. Needs real behavior proof before merge: No inspectable after-fix real behavior proof was provided; a redacted terminal log, copied live output, or screenshot/recording showing a custom CLAUDE_CONFIG_DIR Keychain/file read would be needed, with private paths and tokens redacted. 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.

Label justifications:

  • P2: This is a normal-priority Claude auth-provider feature with limited blast radius, but it touches credential routing and has a blocking scoping defect before merge.
  • merge-risk: 🚨 auth-provider: Merging could route Claude OAuth availability, cache, and delegated refresh through the wrong account when default and custom config-dir accounts coexist.
  • merge-risk: 🚨 security-boundary: The diff broadens which Claude credentials CodexBar reads from files and Keychain services, so account isolation and maintainer-approved auth boundaries need proof before merge.
  • rating: 🧂 unranked krab: Overall readiness is 🧂 unranked krab; proof is 🧂 unranked krab and patch quality is 🧂 unranked krab.
  • status: 📣 needs proof: The PR needs real behavior proof before ClawSweeper can clear the contributor ask. Needs real behavior proof before merge: No inspectable after-fix real behavior proof was provided; a redacted terminal log, copied live output, or screenshot/recording showing a custom CLAUDE_CONFIG_DIR Keychain/file read would be needed, with private paths and tokens redacted. 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

Security concerns:

  • [medium] Account scope can fall out of credential reads — Sources/CodexBarCore/Providers/Claude/ClaudeOAuth/ClaudeOAuthCredentials.swift:226
    Because the new account scope is a TaskLocal set only for loadRecord, other credential helpers can still read or cache the default Claude credentials during custom-account probes and delegated refresh recovery.
    Confidence: 0.82

What I checked:

Likely related people:

  • steipete: Owner comments on the linked multi-account issue and recent commits cover the Claude OAuth history, Keychain prompt, and CLI isolation boundaries this PR changes. (role: owner product-direction reviewer and recent area contributor; confidence: high; commits: 24fe798f3769, 9a2cc6f50d44, 92bbb7a67073; files: Sources/CodexBarCore/Providers/Claude/ClaudeOAuth/ClaudeOAuthCredentials.swift, Sources/CodexBarCore/Providers/Claude/ClaudeUsageFetcher.swift, VISION.md)
  • Yuxin Qiao: Git blame points to this author for the initial Claude OAuth credential store, hardcoded Keychain service, loadRecord flow, and OAuth cache key baseline that the PR modifies. (role: introduced behavior; confidence: high; commits: f2dede1d9086; files: Sources/CodexBarCore/Providers/Claude/ClaudeOAuth/ClaudeOAuthCredentials.swift, Sources/CodexBarCore/KeychainCacheStore.swift)
  • Derek Zeng: Recent Claude OAuth CLI isolation work is adjacent to the delegated-refresh and CLI/runtime boundary affected by account-scoped credential reads. (role: adjacent OAuth isolation contributor; confidence: medium; commits: 3bc978114747; files: Sources/CodexBarCore/Providers/Claude/ClaudeUsageFetcher.swift)
What the crustacean ranks mean
  • 🦀 challenger crab: rare, exceptional readiness with strong proof, clean implementation, and convincing validation.
  • 🦞 diamond lobster: very strong readiness with only minor maintainer review expected.
  • 🐚 platinum hermit: good normal PR, likely mergeable with ordinary maintainer review.
  • 🦐 gold shrimp: useful signal, but proof or patch confidence is still limited.
  • 🦪 silver shellfish: thin signal; proof, validation, or implementation needs work.
  • 🧂 unranked krab: not merge-ready because proof is missing/unusable or there are serious correctness or safety concerns.
  • 🌊 off-meta tidepool: rating does not apply to this item.

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.

How this review workflow works
  • 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.

@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. labels Jul 1, 2026
@steipete

steipete commented Jul 1, 2026

Copy link
Copy Markdown
Owner

Thanks @LawyZheng for investigating this and for adding focused tests. I am closing this implementation while keeping #1756 open for the product decision.

This is an auth/storage feature, so VISION requires owner sign-off. The current decision record in #1812 explicitly holds native credential reads and recommends Phase 1 as an opt-in, read-only cswap --list --json adapter with no Claude credential or Keychain access.

The audited head a82985cf24a170bbfc12b93a64ee4dd54bdb5286 is also not safe as a native-account primitive:

  • Claude Code 2.1.197 derives the Keychain suffix from the NFC-normalized raw config-directory value. The patch canonicalizes the path and collapses values Claude Code treats as distinct, including an explicit default directory and trailing-slash variants, so it can query the wrong service.
  • The TaskLocal scope wraps only loadRecord. Availability checks, invalidation, delegated-refresh sync, persistent-reference matching, probes, fingerprints, prompt/failure state, and throttling can still use default or global state while a custom account is requested.
  • The tests exercise the wrapped load path and use global invalidation, so they do not expose those unscoped paths.
  • This does not provide multi-account product behavior: there is no account enumeration or UI, while usage storage, menu/status items, and widgets remain provider-keyed.

There is no independently useful narrow seam to retain here: the cache-key overload has no approved account consumer, and the remaining changes encode the held native-Keychain design. If native credential discovery is approved later, it should start from an explicit account context threaded through every credential/cache/probe/refresh path and an account-aware usage/UI model, rather than this TaskLocal patch.

Canonical path forward: #1812 and #1756. Thanks again for contributing the investigation.

@steipete steipete closed this Jul 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

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