Name a Claude ring after its account, and give a desktop session to the account that hosts it - #321
Merged
Conversation
…he account that hosts it
The Claude desktop app leaves CLAUDE_CONFIG_DIR unset, so Claude Code
registers every session it hosts in ~/.claude/sessions -- the default
profile's directory -- whichever account the app is signed in to.
Switching account inside the app moves the credential and not the file,
so on a machine with two profiles the second account's work spun the
first account's ring, beside a weekly limit that was correct.
The registry entry names no account. The desktop app does, one directory
per account, and the entry carries the hostSessionId that names it:
~/Library/Application Support/Claude/claude-code-sessions/
<accountUuid>/<organizationUuid>/<hostSessionId>.json
.claude.json already read here for the address carries the matching
accountUuid, so the two join locally -- no network, no keychain, nothing
sent anywhere. ClaudeSessionOwnership does the join: a desktop session
goes to the account that hosts it, a terminal or VS Code session stays
with the directory it was filed in because those inherit the variable
that chose it, and anything unprovable stays where it was. A session on
the wrong ring is a bug; a session on no ring at all is a worse one.
Wired only when there is more than one profile.
displayName came from the directory, which cannot answer the question
two rings raise: the default profile is always ~/.claude, so the account
most people use every day was the one ring with no name on it. The
signed-in address answers it and costs no keychain prompt, so
someone@gmail.com gives "Claude Gmail". The directory stays as the
fallback for a profile signed out or not yet written. Two accounts on
one provider would derive the same name; only a caller holding every
profile can see that, so displayNames(for:) settles it with the whole
address, for the profiles that collide and no others.
Provider ids are untouched, so archived readings and connection choices
survive.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This was referenced Sep 22, 2026
This was referenced Sep 23, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Name a Claude ring after its account, and give a desktop session to the account that hosts it
Two things go wrong on a Mac with two Claude accounts. Both come from the same
place: the directory is treated as the account, and since the Claude desktop app
arrived it is not one.
1. A desktop session is drawn on the wrong ring
The desktop app leaves
CLAUDE_CONFIG_DIRunset, so Claude Code registers everysession it hosts in
~/.claude/sessions— the default profile's directory —whichever account the app is signed in to. Switching account inside the app
moves the credential and not the file.
The effect on a machine with
~/.claude(personal) and~/.claude-work: workdone in the desktop app on the work account spins the personal ring, and the
work ring sits idle with a correct weekly limit beside it. Nothing about the app
says otherwise, and pinning
CLAUDE_CONFIG_DIRdoes not fix it — that variableis global and static, and which account the app is on is neither.
The registry entry carries nothing about the account. But the desktop app keeps
its own record of every session it hosts, one directory per account:
and the registry entry carries that
hostSessionId..claude.json— alreadyread here for
signedInAddress()andorganizationID()— carries the matchingaccountUuidon our side. So the two join, locally, with no network, nokeychain and nothing sent anywhere.
ClaudeSessionOwnershipdoes that join:because those do inherit the variable that chose the directory;
unreadable index, a profile with no
.claude.json, an account no profile onthe machine is signed in to. A session on the wrong ring is a bug; a session
on no ring at all is a worse one.
It is wired only when there is more than one Claude profile. With one, every
call path is byte-for-byte what it was.
2. The ring most people use every day has no name on it
displayNamecame from the directory: the default profile is always~/.claude,so it was just "Claude", and a second login was named for its folder —
Claude (work), whoever is actually signed in to it. Neither answers the onequestion two Claude rings raise.
The signed-in address does, it is already read here for the Settings row, and it
costs no keychain prompt. So
someone@gmail.comgives Claude Gmail andsomeone@acme.co.ukgives Claude Acme. The domain rather than the localpart on purpose: a person's local part is usually the same word on every account
they own; the domain is what separates the personal login from the work one at a
glance.
The old spelling stays as the fallback rather than being dropped — a profile
signed out, or one Claude Code has not written
.claude.jsonfor yet, still hasto be called something, and the directory is all there is then.
Two accounts on one provider would both derive
Claude Gmail, which is worsethan the directory names this replaces. Only a caller holding every profile can
see that clash, so
ClaudeProfile.displayNames(for:)settles it and falls backto the whole address — for the profiles that collide, and only those.
Provider ids are untouched (
claude,claude-<slug>), so archived readings,connection choices and hover-band keys survive.
What changed
Providers/ClaudeProfile.swiftdisplayNamefrom the account;accountLabel;displayNames(for:);accountID();accountUuidonAccountFile.AccountProviders/ClaudeOAuthProvider.swiftdisplayName, so a set of profiles can be named togetherSessions/ClaudeSessionRecord.swiftentrypointandhostSessionID;isDesktopHostedSessions/ClaudeDesktopSessionIndex.swifthostSessionId→ account uuidSessions/ClaudeSessionOwnership.swiftSessions/ClaudeSessionMonitor.swiftrecords(in:)split outApp/AppDelegate.swiftNotes for review
another profile's directory arrives on the next liveness tick instead, two
seconds later. A second descriptor per profile per profile seemed a lot of
machinery to save a delay nobody can see, and the timer is already what covers
what a file event cannot report. Happy to change it.
a moment after Claude Code registers the session, so the first look can
legitimately find nothing; remembering "unknown" would leave the session on
the wrong ring for its whole life. A hit is kept for good — which account
hosted a session is decided when it is created.
hostSessionIdis checked, not trusted: it is a path component from a fileanother program writes.
ClaudeAccountNameTests(naming, including the collision and every waythere is no label to give) and
ClaudeSessionOwnershipTests(the index, whoclaims what, and the end-to-end read — including that with no ownership the
read is exactly what it always was).
🤖 Generated with Claude Code