feat(cc-config): switch between Claude Code accounts, keeping the workspace
Adds a "Switch profile" button left of Backups on /settings?tool=claude-code. A *profile* is one Anthropic account - one quota - saved under a name, so the local `claude` CLI can be pointed at a different account without disturbing anything else. A switch swaps exactly two identity artifacts and nothing more: `claudeAiOauth` inside `<CLAUDE_HOME>/.credentials.json`, and `oauthAccount` + `userID` inside the `.claude.json` config file(s). Those files are key-merged in place, and only where they already exist, so `projects`/`numStartups`/everything else survives. Skills, subagents, commands, memory, plugins, marketplaces, MCP servers and settings are never read or written here - that is what makes a switch keep the whole workspace and change only the account. `mcpOAuth` stays put too: those are the machine's MCP server tokens, not account identity. Profiles are one JSON file each under `<CLAUDE_HOME>/agenthub-profiles/` (dir 0700, files 0600, atomic temp+rename) and hold live OAuth tokens, so no route ever returns token material - `GET /profiles` answers identity metadata only. The dashboard cannot log an account in; only `claude auth login` mints tokens. Adding a profile is therefore always "log in from a terminal, then snapshot what the CLI wrote". `activateProfile` re-snapshots the outgoing account first so tokens Claude Code refreshed since its last save are not lost, and refuses (`EUNSAVED`) to switch away from an account that was never saved - that would discard the only copy of its credentials. A switch applies to new sessions only, since a running session rewrites the config on exit. Routes: GET/POST/DELETE /api/cc-config/profiles, POST /profiles/activate. Verified: server 1420/1420 (14 new, incl. token-free responses and 0600/0700 permissions), client 459/459.
This commit is contained in:
+10
@@ -1580,6 +1580,10 @@ GET /api/cc-config/settings
|
||||
GET /api/cc-config/memory
|
||||
GET /api/cc-config/file?path=<absolute-path>
|
||||
GET /api/cc-config/backups[?scope=&type=]
|
||||
GET /api/cc-config/profiles
|
||||
POST /api/cc-config/profiles Body: { name }
|
||||
POST /api/cc-config/profiles/activate Body: { name }
|
||||
DELETE /api/cc-config/profiles Body: { name }
|
||||
PUT /api/cc-config/file Body: { scope, type, name?, content }
|
||||
DELETE /api/cc-config/file Body: { scope, type, name? }
|
||||
PATCH /api/cc-config/settings Body: { scope, set?: { "dotted.key": value }, unset?: ["dotted.key"] }
|
||||
@@ -1602,6 +1606,12 @@ POST /api/cc-config/marketplaces/action Body: { name, action: update|remove }
|
||||
|
||||
`PUT /api/cc-config/keybindings` edits `~/.claude/keybindings.json` from a structured list of context groups (`{ groups: [{ context, bindings: [{ key, action }] }] }`). The server backs the file up first (under `<CLAUDE_HOME>/cc-config-backups/keybindings/`), preserves any top-level metadata (`$schema`/`$docs`), and replaces only the `bindings` array; duplicate contexts or duplicate keys within a context return `EBADCONTENT`. `keybindings.json` is replaced wholesale here because the live CLI does not rewrite it mid-session; `settings.json`, which it does, is only ever key-merged (see `PATCH /settings` above).
|
||||
|
||||
**Auth profiles (`/profiles*`).** A *profile* is one Anthropic account — one quota — under a name. Switching swaps exactly two identity artifacts and nothing else: `claudeAiOauth` inside `<CLAUDE_HOME>/.credentials.json`, and `oauthAccount` + `userID` inside the `.claude.json` config file(s) (`<CLAUDE_HOME>.json` and `<CLAUDE_HOME>/.claude.json`, each rewritten only if it already exists, key-merged so `projects`/`numStartups`/everything else survives). Skills, subagents, commands, memory, plugins, marketplaces, MCP servers and settings are never read or written here — that is what makes a switch keep the whole workspace and change only the account. `mcpOAuth` also stays put: those are the machine's MCP server tokens, not account identity.
|
||||
|
||||
Profiles live one JSON file per profile under `<CLAUDE_HOME>/agenthub-profiles/` (dir `0700`, files `0600`) and hold live OAuth tokens, so **no route ever returns token material** — `GET /profiles` answers `{ items, active }` with identity metadata only (`name`, `savedAt`, `active`, `accountUuid`, `email`, `displayName`, `organization`, `seatTier`, `subscriptionType`, `expiresAt`). `name` must match `^[a-zA-Z0-9][a-zA-Z0-9._-]{0,39}$` (it is the filename).
|
||||
|
||||
The dashboard **cannot log an account in** — only `claude auth login` mints tokens. Adding a profile is therefore always "log in from a terminal, then `POST /profiles` to snapshot what the CLI wrote". `POST /profiles/activate` re-snapshots the outgoing account into its own profile first (reported as `savedOutgoing`), so tokens Claude Code refreshed since the last save are not lost, and returns `{ ok, name, savedOutgoing, written }`. Errors: `EBADNAME` (400), `ENOTFOUND` (404 — no such profile), `EBADJSON` (409 — a config file on disk is unparseable; nothing is written), `ENOAUTH` (409 — nobody is logged in to snapshot), `EUNSAVED` (409 — the live account is not saved as a profile, and switching away would discard the only copy of its credentials), `EACTIVE` (409 — refusing to delete the active profile). Mutations broadcast `cc_config_changed` with `type: "profile"`. A switch only takes effect for **new** Claude Code sessions, and a session still running will rewrite the config on exit — close them first.
|
||||
|
||||
Backup paths look like `<root>/cc-config-backups/<type>/<base>.<ISO>.bak[.dir]` — outside the directories Claude Code scans, so a deleted skill cannot resurface as a backup-named one. The Backups modal in the UI auto-builds `mv` restore commands.
|
||||
|
||||
### Run Claude via tmux+PTY
|
||||
|
||||
Reference in New Issue
Block a user