Skip to content

Account-first Phase 3: Claude Cowork / Desktop accounts - #1032

Closed
robinebers wants to merge 5 commits into
mainfrom
account-first-phase-3
Closed

Account-first Phase 3: Claude Cowork / Desktop accounts#1032
robinebers wants to merge 5 commits into
mainfrom
account-first-phase-3

Conversation

@robinebers

Copy link
Copy Markdown
Owner

TL;DR

Cowork sessions become account-aware: each sandbox names the account that ran it, and a Claude Desktop login distinct from every CLI login gets its own card — backed read-only by Desktop's org-pinned Safe Storage credentials, with its Cowork sessions as the card's spend.

What was happening

  • The Claude card counted every Cowork sandbox on the machine, no matter which account ran the session — a Desktop signed into a different account silently bled its spend into the default card.
  • A Claude Desktop login with no CLI counterpart was invisible: no card, no limits, no spend.
  • The Desktop Safe Storage fallback always followed Desktop's active org, which becomes wrong the moment more than one Claude account exists on the machine.

What this changes

  • ClaudeCoworkDiscovery (new): a launch-time walk over Cowork's session sandboxes that reads each sandbox's own .claude.json identity file (no keychain, no credentials, 400ms budget). The walk itself is shared with the spend scanner, so discovery and scanning can never see different sandbox sets.
  • Assembly routing: sandboxes naming the default account stay exactly where they've always been; sandboxes naming a known config-dir account attach as that card's extra log roots; a distinct account becomes one Desktop-backed card. The moment any non-default sandbox exists, the default card's Cowork walk is partitioned so another account's sessions can't bleed into its spend.
  • Org-pinned Desktop credentials: ClaudeDesktopAuthStore.load accepts an organization pin; a Desktop-backed card reads exactly its account's cached token, never Desktop's active org. New ClaudeCredentialScope.desktopOnly(organization:).
  • Default card stays safe: once extra Claude cards exist, the default card's Desktop fallback is pinned to its own org (parsed from its identity key) — or disabled when that org is unknown — instead of blindly borrowing whatever org Desktop has active.
  • ClaudeLogUsageScanner: gains coworkRootsOverride to scan an explicit sandbox set (the partition); cache identity includes the partition so changing it invalidates correctly.
  • New .desktop source kind on account records; Desktop-backed cards reuse the whole existing account-card pipeline (layout, rename, iCloud identity, CLI/API matching).

Heads-up

  • Signing Desktop out removes the card the same way a deleted config dir does (owner decision: cards render only while their source is found).
  • A distinct Cowork account with no org pin gets no card (Desktop caches tokens per org, so an unpinned read could fetch another account's usage) — but its sandboxes still stay out of the default card's spend.
  • Sandboxes with no identity file (pre-identity sessions, mid-creation) keep counting on the default card, exactly as the built-in walk always has.

Tests

  • 13 new tests: sandbox identity reading, all four assembly routing paths (default / fold-onto-config-dir-card / distinct-account card / no-org-pin skip), org-pinned Desktop load, .desktopOnly scope, pinned standard fallback, and the scanner partition.
  • Full suite: 1292 tests pass.
  • Live run on this machine: default-only setup unchanged (no sandboxes from other accounts → no partition, byte-identical walk).

Made with Cursor

Cowork sandboxes now carry per-sandbox identity: sessions from the default
login stay on the default card, sessions from a known config-dir account
attach as its log roots, and a distinct Desktop login becomes its own card
backed by Desktop's org-pinned Safe Storage credentials. The default card's
Cowork walk is partitioned the moment another account's sandboxes exist so
no spend bleeds between cards.

Co-authored-by: Cursor <cursoragent@cursor.com>
Comment thread Sources/OpenUsage/Services/ProviderAccountAssembly.swift
Comment thread Sources/OpenUsage/Services/ProviderAccountAssembly.swift
@robinebers robinebers added the gate-passed Passed the automated PR gatekeeper checks (gatekeeper skips it on later runs) label Jul 19, 2026 — with Cursor
…ing on a truncated walk

Two review findings: an identity file that omits its org half must not split
one login into two cards (routing now compares uuid-first via
sameClaudeAccount), and a cowork walk that hits its time budget must not
drive routing from a partial sandbox list (the pass skips wholesale and
retries next launch).

Co-authored-by: Cursor <cursoragent@cursor.com>
Comment thread Sources/OpenUsage/Providers/Claude/ClaudeLogUsageScanner.swift
Comment thread Sources/OpenUsage/Providers/ProviderCatalog.swift
Review follow-up: the partition (and scoped cards' log roots) is
launch-frozen by design — a live walk would count another account's
brand-new sessions on the wrong card until routing catches up, the exact
bleed the partition exists to prevent. Missing-until-relaunch is the safer
failure; files inside known sandboxes still update live.

Co-authored-by: Cursor <cursoragent@cursor.com>

@cursor cursor 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.

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 015a575. Configure here.

Comment thread Sources/OpenUsage/Providers/ProviderCatalog.swift Outdated
robinebers and others added 2 commits July 19, 2026 15:26
Review finding: a distinct Cowork account without an org pin builds no card,
so gating the unpinned fallback on claudeCards.isEmpty left the default card
free to follow Desktop's active org — that account's usage pool. The gate now
also checks the partition, which is set whenever another account's sandboxes
exist.

Co-authored-by: Cursor <cursoragent@cursor.com>
Field find on a Max 20x account showing "Max 5x": v2 held the current login
only as an expired entry plus a live profile-only leftover with stale 5x
tier metadata, while the login's live full-scope token sat in v1 — and the
v2-first short-circuit never looked. Both generations now compete in one
ranked pool: token quality outranks cache generation, generation only breaks
quality ties (still ahead of raw expiry). The v2-tombstone-suppresses-v1
rule is unchanged.

Co-authored-by: Cursor <cursoragent@cursor.com>
@robinebers

Copy link
Copy Markdown
Owner Author

Just discovered a bug that when logged into Claude Code but changing the account while OpenUsage is running, that this logic now shows 2x the same account despite Cowork still having knowledge over two different accounts. This needs fixing.

@robinebers

Copy link
Copy Markdown
Owner Author

Bug: bare-id Claude account disappears after default-home swap

What I expected

Two Claude cards (as before the swap):

  1. Team (SUNSTORY) — Desktop / Cowork
  2. Personal — default home ~/.claude

What actually happens

Only one Claude card renders after Personal takes over the default home. Team is discovered every launch, then intentionally dropped.

Repro (this machine)

  1. Registry has two Claude accounts, same user UUID, different orgs:
    • claude (Team / SUNSTORY) — source: desktop only, no default home
    • claude@b2d3867d (Personal) — source: defaultHome at ~/.claude with the badge
  2. Launch account-first-phase-3 / this PR’s build.
  3. Logs every launch:
accounts: claude default identity resolved (claude@b2d3867d)
discovery: cowork partition — default keeps 21 sandbox dir(s), 1 other account(s) found
WARN: discovery: the claude record's account now lives outside the default home; its card is unavailable until swap support lands
  1. Refresh batch is a single Claude provider (claude), not two. UI shows one Claude card.

When it still worked

Through ~2026-07-30 both cards refreshed (claude + claude@b2d3867d). After Personal moved into ~/.claude, Team only exists via Cowork/Desktop and hits the Phase 3 guard.

Root cause

In ProviderAccountAssembly, extra-card build skips any planned card whose reconciled record id is the bare family id:

guard record.id != "claude" else {
    // The bare record's account has moved out of the default home into a side login
    // while another account occupies the default. The bare CARD is the default home's
    // runtime, so this record can't render under its own id this launch. Proper swap
    // support re-points this in Phase 4; until then the parked account stays hidden.
    AppLog.warn(.config, "discovery: the claude record's account now lives outside the default home; its card is unavailable until swap support lands")
    continue
}

So:

  • Team still owns bare id claude (first account at default home historically).
  • Personal holds the default badge under claude@b2d3867d.
  • Team is found as a Desktop-backed extra → planned card exists → reconcile maps it to id claudeskipped.
  • Personal is default, not an “extra”, so no second card is minted for claude@b2d3867d either.
  • Net: only the default-home runtime card remains; Team is gone from the UI.

This matches the plan’s “swap re-points sources” story, but Phase 4 isn’t here yet — and today that gap is user-visible data loss (a known, still-found account with rename/layout/history simply stops rendering).

Expected fix direction (Phase 4 or a Phase 3 follow-up)

When the bare-id account no longer holds the default home:

  • Keep its card rendering under a stable non-bare id (or re-point the bare id’s runtime to the badge holder and mint/keep a side id for the parked account), so both accounts stay visible whenever their sources are found.
  • Don’t require the parked account to vanish until cswap lands.

Happy to help verify a patch against this machine’s registry state.

@robinebers

Copy link
Copy Markdown
Owner Author

Superseded by #1140, which rebuilds Claude multi-account support on the current main branch with account-owned runtimes, Desktop and CLI discovery, account-safe iCloud synchronization, and comprehensive regression coverage.

@robinebers robinebers closed this Aug 24, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

docs gate-passed Passed the automated PR gatekeeper checks (gatekeeper skips it on later runs) provider tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant