feat: several Claude subscriptions, bound per session - #10
Open
pocharlies wants to merge 1 commit into
Open
Conversation
One OpenCode server can drive several Claude subscriptions at once, with each session pinned to one: this chat on `work`, that one on `personal`. Each account is a CLAUDE_CONFIG_DIR — a self-contained Claude CLI home with its own credentials, transcripts and settings. That shape is forced by the rotation constraint this codebase already documents in auth-login.ts: Anthropic rotates the refresh token on every use, and a chain with two owners gets the whole grant revoked for replay. Giving each account its own CLI home keeps exactly one owner per chain — the CLI — so accounts cannot race each other's rotation. The plugin reads those credentials and never rotates them. Selection rides the model id (`opus@personal`, named `Opus 5 · Personal`) so it flows through the existing chat.headers → EFFORT_HEADER path and appears in the host's model picker with no UI work. The first turn binds the session; later turns stay on that account even when the request carries none. Everything previously global is now keyed by account: - rate-limit state and its 429 fast-fail gate — an exhausted subscription used to block every other account, which defeats having them. Existing single-account stores migrate on read. - session bindings, and resume lookups against the owning account's transcript dir; moving accounts drops the stale resume target rather than continuing a foreign conversation. - the pre-flight credential probe and credential reads, which now refuse to fall back to the ambient ~/.claude for a scoped account — that would silently run the turn on the wrong subscription. Declaring no accounts leaves behaviour unchanged: same model ids, same stores, same everything. Configure via the `accounts` plugin option, OPENCODE_CLAUDE_ACCOUNTS, or accounts.json. Also isolates XDG_DATA_HOME for the test run. The suite was reading the operator's real accounts.json — so "nothing configured" stopped being true once they had accounts — writing fixture sessions into their store, and unlinking their debug.log.
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.
One OpenCode server drives several Claude subscriptions at once, with each
session pinned to one — this chat on
work, that one onpersonal.Each account is a
CLAUDE_CONFIG_DIR. That shape is forced by the rotationconstraint this codebase already documents in
auth-login.ts: Anthropic rotatesthe refresh token on every use, and a chain with two owners gets the whole grant
revoked for replay. Giving each account its own CLI home keeps exactly one owner
per chain — the CLI — so accounts cannot race each other's rotation. The plugin
reads those credentials and never rotates them.
Selection rides the model id.
opus@personal, shown asOpus 5 · Personal,flows through the existing
chat.headers→EFFORT_HEADERpath, so it appearsin the host's picker with no UI work. The first turn binds the session; later
turns stay on that account even when the request carries none.
Everything previously global is now keyed by account:
used to block every other account, which defeats having them. Existing
single-account stores migrate on read.
transcript dir. Moving accounts drops the stale resume target rather than
continuing a foreign conversation in another home.
to the ambient
~/.claudefor a scoped account — that would silently run theturn on the wrong subscription.
Declaring no accounts changes nothing: same model ids, same stores, same
behaviour. Configure with the
accountsplugin option,OPENCODE_CLAUDE_ACCOUNTS, oraccounts.json.One caveat worth stating:
CLAUDE_CONFIG_DIRalso relocates settings, skillsand the user-level
CLAUDE.md, so shared files need symlinking into each home.Documented in the README.
Also isolates
XDG_DATA_HOMEfor the test run — the suite was reading theoperator's real
accounts.json(so "nothing configured" stopped being true oncethey had accounts), writing fixture sessions into their store, and unlinking
their
debug.log.bun testpasses andtscis clean. Independent of #9.