feat(desktop,host-service): provision account profiles as complete environments with shared history
Summary
Secondary provider accounts (the CLAUDE_CONFIG_DIR / CODEX_HOME profile dirs behind the Usage tab's account switching, #6585) were bare logins: an empty install with no skills, plugins, MCP servers, or settings — and a separate, fragmented conversation history. This PR provisions each profile from the default account so switching accounts keeps the whole environment:
- Config sharing (
packages/agent-setup/src/provider-profiles.ts+profile-sharing.ts) — skills, plugins, agents, commands, output-styles, settings, MCP servers,CLAUDE.md; Codexprompts/,config.toml,AGENTS.md. - Session-history sharing (
packages/host-service/src/trpc/router/usage/profile-share.ts) — oneprojects/transcript tree,--resumelist, and prompt history across every Claude login.
Why token-swapping wasn't the design
Keeping one config dir and swapping credentials on switch was rejected: refresh tokens rotate on use, and a running session refreshes and rewrites the credential slot mid-session, so a swapped-in token gets clobbered and a parked copy goes stale (usage/claude.ts documents this as the "tokens are read-only" hard rule). Sharing state instead lets the provider CLIs own every login end to end.
How It Works
The rule: symlink where a symlink survives, copy/merge where it can't.
- Symlinked — directories, and
history.jsonl(append-only, written in place): capability dirs are linked at the default account's, so a skill or plugin installed later shows up in every account with no sync step; session dirs are linked so history is one tree. - Copied / key-merged — files the CLIs write with write-tmp-then-rename (
settings.json,.claude.json,CLAUDE.md,config.toml): a rename would replace a symlink with a real file and silently fork the config. A per-profile ledger (.superset-profile.json) records exactly what provisioning wrote, so a value the user changes inside a profile is never clobbered by the next run. - Never shared: credentials/keychain,
oauthAccount/userIDidentity, auth-related settings keys (apiKeyHelper,forceLoginMethod, …), credential env vars, Superset's lifecycle hooks (written per profile), and runtime dirs (daemon/,cache/,telemetry/). - Existing session state is merged, not clobbered: rename aside → symlink lands → files move into
~/.claudeone by one. Renames preserve inodes, so a live session's open transcripts stay valid and its paths resolve through the link; conflicts wait in a.superset-mergedir and are retried.
Provisioning is idempotent and runs when an account is added (prepareAccount, generalized from prepareClaudeProfile to both providers), when one is selected, and at host boot for the selected accounts (usage/account-provisioning.ts). profile-seed.ts is absorbed into the .claude.json merge (forced hasCompletedOnboarding). The usage-history scanner realpath-dedupes its scan roots so a symlinked projects/ isn't walked and parsed once per profile. Docs: docs/agent-tooling.md gains a "Provider accounts (multi-login)" section.
Manual QA
Run live against a real secondary profile (~/.claude-work) while a session was running under it:
- Live session survived the projects/sessions migration and kept writing (transcript readable through the link)
-
projects/merged cleanly — 1,213 transcripts unified, zero conflicts;history.jsonlmerged losslessly (26,757 + 39 = 26,796 lines) -
claude --resumeunder the default account and underCLAUDE_CONFIG_DIR=~/.claude-workshows the identical session picker (recorded demo) - Full converged provisioning re-run on the provisioned profile is a clean no-op:
skills/=unchanged plugins/=user-owned agents/=unchanged commands/=unchanged CLAUDE.md=unchanged settings.json=unchanged .claude.json=unchanged - User-owned surfaces respected (real
plugins/dir left alone; the bundled Superset plugin is then written into it directly)
Testing
bun test packages/host-service/src/trpc/router/usage/— 17 pass (merge, conflict-leftover, interrupted-merge recovery, history append, guards; removal never deletes through shared links)bun test packages/agent-setup/src/— 143 pass (ledger semantics, link/sync/merge outcomes, settings/env filtering, onboarding force, Codex)bun run typecheckin host-service, agent-setup, and desktop- biome clean on changed files
Design Decisions
- Two mechanisms instead of one: symlinks alone silently fork on the first rename-write of a JSON file; copies alone go stale for capability dirs. Each surface gets the one that survives its CLI's write pattern.
- Ledger over blind overwrite: per-profile divergence is legitimate (a profile-specific setting) — provisioning only overwrites what it last wrote.
Rollback
Revert the commit. Already-linked profiles keep working — the links are plain symlinks and copies the CLIs follow natively; no schema or persisted-state changes.
Summary by CodeRabbit
-
New Features
- Claude and Codex profiles inherit skills, plugins, MCP servers, and settings from the default account.
- Claude session history and state can be shared across profiles while preserving credentials and identity.
- Profile setup runs automatically at startup and when switching accounts.
- Shared configuration safely supports existing profiles and symlinked directories.
-
Bug Fixes
- Duplicate or symlinked log locations are scanned only once.
- Removing profiles no longer deletes linked shared data.
-
Documentation
- Added Usage documentation and clarified shared versus profile-specific data.