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:
+261
@@ -2903,6 +2903,103 @@ components:
|
||||
description: Backups across the requested scope/type buckets, sorted newest first.
|
||||
items:
|
||||
$ref: '#/components/schemas/CcConfigBackup'
|
||||
CcConfigAccountIdentity:
|
||||
type: object
|
||||
description: Identity of one Anthropic account. Deliberately token-free - the auth-profile endpoints never return credential material, only enough to tell accounts apart.
|
||||
properties:
|
||||
accountUuid:
|
||||
type: string
|
||||
nullable: true
|
||||
example: c9b05d52-391c-449b-a6b1-e161edea7d8a
|
||||
email:
|
||||
type: string
|
||||
nullable: true
|
||||
example: you@example.com
|
||||
displayName:
|
||||
type: string
|
||||
nullable: true
|
||||
example: Vĩ
|
||||
organization:
|
||||
type: string
|
||||
nullable: true
|
||||
example: Smartgift
|
||||
seatTier:
|
||||
type: string
|
||||
nullable: true
|
||||
example: team_standard
|
||||
subscriptionType:
|
||||
type: string
|
||||
nullable: true
|
||||
example: max
|
||||
expiresAt:
|
||||
type: integer
|
||||
nullable: true
|
||||
description: Access-token expiry, epoch ms. A switch still works past it - the refresh token is stored too.
|
||||
example: 1788776815474
|
||||
CcConfigProfile:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/CcConfigAccountIdentity'
|
||||
- type: object
|
||||
required:
|
||||
- name
|
||||
- active
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
description: Profile name; also its filename under `<CLAUDE_HOME>/agenthub-profiles/`.
|
||||
pattern: ^[a-zA-Z0-9][a-zA-Z0-9._-]{0,39}$
|
||||
example: work
|
||||
savedAt:
|
||||
type: integer
|
||||
nullable: true
|
||||
description: When the profile was last snapshotted, epoch ms.
|
||||
example: 1788743353379
|
||||
active:
|
||||
type: boolean
|
||||
description: True when this profile's account is the one logged in right now.
|
||||
CcConfigProfilesResponse:
|
||||
type: object
|
||||
required:
|
||||
- items
|
||||
- active
|
||||
properties:
|
||||
items:
|
||||
type: array
|
||||
description: Saved profiles, sorted by name.
|
||||
items:
|
||||
$ref: '#/components/schemas/CcConfigProfile'
|
||||
active:
|
||||
nullable: true
|
||||
description: The account logged in right now, or null when nobody is.
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/CcConfigAccountIdentity'
|
||||
CcConfigProfileActivateResult:
|
||||
type: object
|
||||
required:
|
||||
- ok
|
||||
- name
|
||||
- savedOutgoing
|
||||
- written
|
||||
properties:
|
||||
ok:
|
||||
type: boolean
|
||||
example: true
|
||||
name:
|
||||
type: string
|
||||
example: work
|
||||
savedOutgoing:
|
||||
type: string
|
||||
nullable: true
|
||||
description: Profile the outgoing account was re-snapshotted into (so tokens Claude Code refreshed since its last save are not lost), or null when the target was already active.
|
||||
example: personal
|
||||
written:
|
||||
type: array
|
||||
description: Absolute paths rewritten by the switch.
|
||||
items:
|
||||
type: string
|
||||
example:
|
||||
- /Users/son/.claude/.credentials.json
|
||||
- /Users/son/.claude.json
|
||||
RunHandle:
|
||||
type: object
|
||||
description: Live, in-memory view of a spawned `claude` subprocess. Returned by GET /api/run (in `items`), POST /api/run (201), and GET /api/run/{id}. Backed by the spawner's handle map, which reaps each handle 5 minutes after the process exits — after that the run is only visible via GET /api/run/history.
|
||||
@@ -8090,6 +8187,170 @@ paths:
|
||||
isDir: false
|
||||
mtime: 1718900000000
|
||||
size: 2048
|
||||
/api/cc-config/profiles:
|
||||
get:
|
||||
tags:
|
||||
- CcConfig
|
||||
summary: List Claude Code auth profiles
|
||||
description: Read-only. A *profile* is one Anthropic account - one quota - saved under a name, so the local `claude` CLI can be switched between accounts while every other config surface stays put. Profiles live one JSON file per profile under `<CLAUDE_HOME>/agenthub-profiles/` (dir 0700, files 0600) and hold live OAuth tokens, so this endpoint returns identity metadata ONLY - never token material.
|
||||
operationId: ccConfigGetProfiles
|
||||
responses:
|
||||
'200':
|
||||
description: Saved profiles plus the account logged in right now.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/CcConfigProfilesResponse'
|
||||
'409':
|
||||
description: '`EBADJSON` - a credentials/config file on disk is not valid JSON.'
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
post:
|
||||
tags:
|
||||
- CcConfig
|
||||
summary: Save the logged-in account as a profile
|
||||
description: 'Snapshots the account currently logged in (its `claudeAiOauth` blob plus `oauthAccount`/`userID`) into a profile, overwriting a profile of the same name. The dashboard CANNOT log an account in - only `claude auth login` mints tokens - so adding another account is always: log in from a terminal, then call this to capture what the CLI wrote.'
|
||||
operationId: ccConfigSaveProfile
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
required:
|
||||
- name
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
pattern: ^[a-zA-Z0-9][a-zA-Z0-9._-]{0,39}$
|
||||
description: Profile name; also its filename.
|
||||
example: work
|
||||
responses:
|
||||
'200':
|
||||
description: The saved profile's identity.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/CcConfigAccountIdentity'
|
||||
- type: object
|
||||
properties:
|
||||
ok:
|
||||
type: boolean
|
||||
name:
|
||||
type: string
|
||||
'400':
|
||||
description: '`EBADNAME` - the name is not filename-safe.'
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
'409':
|
||||
description: '`ENOAUTH` - no Claude account is logged in to snapshot; `EBADJSON` - a config file is unparseable.'
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
delete:
|
||||
tags:
|
||||
- CcConfig
|
||||
summary: Forget a saved auth profile
|
||||
description: 'Deletes the stored profile file. The identifier travels in the body, not the URL (same reason as `DELETE /file`). Refuses the ACTIVE profile: it holds the live account''s only credential copy outside the files a switch is about to overwrite.'
|
||||
operationId: ccConfigDeleteProfile
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
required:
|
||||
- name
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
example: work
|
||||
responses:
|
||||
'200':
|
||||
description: Profile deleted.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
ok:
|
||||
type: boolean
|
||||
name:
|
||||
type: string
|
||||
example:
|
||||
ok: true
|
||||
name: work
|
||||
'400':
|
||||
description: '`EBADNAME` - the name is not filename-safe.'
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
'404':
|
||||
description: '`ENOTFOUND` - no profile by that name.'
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
'409':
|
||||
description: '`EACTIVE` - refusing to delete the profile that is currently active.'
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
/api/cc-config/profiles/activate:
|
||||
post:
|
||||
tags:
|
||||
- CcConfig
|
||||
summary: Switch the local CLI to another Anthropic account
|
||||
description: |-
|
||||
Makes a saved profile the live account. Swaps EXACTLY two identity artifacts and nothing else: `claudeAiOauth` inside `<CLAUDE_HOME>/.credentials.json`, and `oauthAccount` + `userID` inside the `.claude.json` config file(s) (key-merged in place, and only where the file already exists, so `projects`/`numStartups`/everything else survives). Skills, subagents, commands, memory, plugins, marketplaces, MCP servers and settings are never touched - that is what makes a switch keep the whole workspace and change only the quota. `mcpOAuth` stays put too (machine MCP tokens, not account identity).
|
||||
|
||||
The outgoing account is re-snapshotted into its own profile FIRST, so tokens Claude Code refreshed since its last save are not lost. A switch takes effect for NEW Claude Code sessions only, and a session still running will rewrite the config on exit - close them first.
|
||||
operationId: ccConfigActivateProfile
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
required:
|
||||
- name
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
example: work
|
||||
responses:
|
||||
'200':
|
||||
description: The switch completed; `written` lists the rewritten paths.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/CcConfigProfileActivateResult'
|
||||
'400':
|
||||
description: '`EBADNAME` - the name is not filename-safe.'
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
'404':
|
||||
description: '`ENOTFOUND` - no profile by that name.'
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
'409':
|
||||
description: '`EUNSAVED` - the account logged in right now is not saved as a profile, and switching away would discard the only copy of its credentials; `EBADJSON` - a config file is unparseable, nothing was written.'
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
/api/run:
|
||||
get:
|
||||
tags:
|
||||
|
||||
Reference in New Issue
Block a user