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:
@@ -1509,6 +1509,38 @@ export const api = {
|
||||
*/
|
||||
backups: (params?: { scope?: "user" | "project"; type?: CcArtifactType }) =>
|
||||
requestBackupsHelper(params),
|
||||
/**
|
||||
* GET /api/cc-config/profiles - saved auth profiles + the live account.
|
||||
* @returns `{ items, active }` — {@link CcProfile}s and the currently
|
||||
* logged-in {@link CcAccountIdentity} (null when nobody is logged in).
|
||||
*/
|
||||
profiles: () =>
|
||||
request<{ items: CcProfile[]; active: CcAccountIdentity | null }>("/cc-config/profiles"),
|
||||
/**
|
||||
* POST /api/cc-config/profiles - snapshot the logged-in account under
|
||||
* `name`, overwriting a profile of that name.
|
||||
*/
|
||||
saveProfile: (name: string) =>
|
||||
request<{ ok: true; name: string } & CcAccountIdentity>("/cc-config/profiles", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ name }),
|
||||
}),
|
||||
/**
|
||||
* POST /api/cc-config/profiles/activate - make a saved profile the account
|
||||
* the local `claude` CLI runs as. The outgoing account is re-snapshotted
|
||||
* into its own profile first (`savedOutgoing`).
|
||||
*/
|
||||
activateProfile: (name: string) =>
|
||||
request<{ ok: true; name: string; savedOutgoing: string | null; written: string[] }>(
|
||||
"/cc-config/profiles/activate",
|
||||
{ method: "POST", body: JSON.stringify({ name }) }
|
||||
),
|
||||
/** DELETE /api/cc-config/profiles - forget a saved profile (never the active one). */
|
||||
deleteProfile: (name: string) =>
|
||||
request<{ ok: true; name: string }>("/cc-config/profiles", {
|
||||
method: "DELETE",
|
||||
body: JSON.stringify({ name }),
|
||||
}),
|
||||
/**
|
||||
* GET /api/cc-config/plugins/available - every plugin offered by a
|
||||
* registered marketplace, from `claude plugin list --available --json`.
|
||||
@@ -2261,6 +2293,30 @@ export interface CcBackup {
|
||||
project?: string; // present for scope === "auto-memory"
|
||||
}
|
||||
|
||||
/** Identity of one Anthropic account, as reported by the auth-profile
|
||||
* endpoints. Deliberately token-free — the server never sends credentials to
|
||||
* the browser, only enough to tell the accounts apart. */
|
||||
export interface CcAccountIdentity {
|
||||
accountUuid: string | null;
|
||||
email: string | null;
|
||||
displayName: string | null;
|
||||
organization: string | null;
|
||||
seatTier: string | null;
|
||||
subscriptionType: string | null;
|
||||
/** Access-token expiry, epoch ms; a switch still works past it (the refresh
|
||||
* token is stored too). */
|
||||
expiresAt: number | null;
|
||||
}
|
||||
|
||||
/** One saved auth profile from GET /api/cc-config/profiles. */
|
||||
export interface CcProfile extends CcAccountIdentity {
|
||||
name: string;
|
||||
/** When the profile was last snapshotted, epoch ms. */
|
||||
savedAt: number | null;
|
||||
/** True when this profile's account is the one logged in right now. */
|
||||
active: boolean;
|
||||
}
|
||||
|
||||
/** Scope filter accepted by most `ccConfig` list endpoints; "all" merges
|
||||
* user + project scope in one response. */
|
||||
export type CcScope = "user" | "project" | "all";
|
||||
|
||||
Reference in New Issue
Block a user