The previous helper moved the private account's OAuth token into the work profile and had to be given back afterwards. Only the credentials moved, so plugins and settings stayed behind, and a session that exited at the wrong moment left the swap half-done. An account is a whole profile directory here, and switching points CLAUDE_CONFIG_DIR at another one of them. Nothing is copied, so nothing has to be restored, and the plugins of an account come with it. A wrapper in ~/.local/bin makes the single `claude` command follow the switch, including for the hooks and subprocesses a shell alias would never reach. Each account also gets a Headroom proxy of its own -- its own port, its own workspace, its own service -- because one shared proxy would collect the work of both accounts in one cache. The router publishes them as <account>.headroom.localhost, gives the bare domain to whichever account is active, and keeps resolving the per-session names the enqs-jail plugin publishes. Status is a report rather than two lines: the parts of this setup fail quietly, and a proxy that is not running, a wrapper that is shadowed or two routers on one port are all invisible from a terminal.
112 lines
4.7 KiB
Markdown
112 lines
4.7 KiB
Markdown
# claude-account
|
|
|
|
One `claude` command, several accounts.
|
|
|
|
Each Claude account on this machine is a whole Claude Code profile: its own
|
|
login, settings, plugins, projects and memories. `claude-account switch` picks
|
|
which of them the `claude` command runs as, and every account gets a Headroom
|
|
proxy of its own, reachable under a name that does not change.
|
|
|
|
```sh
|
|
claude-account setup # wrapper, a proxy per account, the router
|
|
claude-account switch work # `claude` runs as the work account from now on
|
|
claude-account status # everything, and everything that is wrong
|
|
```
|
|
|
|
## What it replaces
|
|
|
|
The previous helper lent the private account's OAuth token to the work profile
|
|
and had to be given back afterwards. Only the credentials moved, plugins and
|
|
settings stayed behind, and a session that exited at the wrong moment left the
|
|
swap half-done. Here nothing moves: an account is a directory, and switching
|
|
means pointing `CLAUDE_CONFIG_DIR` at another one. There is nothing to restore,
|
|
and the plugins of an account come with it.
|
|
|
|
## How the switch reaches `claude`
|
|
|
|
`~/.local/bin/claude` is a small wrapper that asks this tool which profile is
|
|
active, exports `CLAUDE_CONFIG_DIR` and hands over to the real `claude`. A
|
|
wrapper and not a shell alias, because an alias only exists in an interactive
|
|
shell: hooks, editors and subprocesses call `claude` without one and must land
|
|
in the same account as the terminal.
|
|
|
|
A `CLAUDE_CONFIG_DIR` that is already set wins over the wrapper, so a session
|
|
started deliberately in another profile -- a jail does this -- stays where it
|
|
was put. Sessions that are already open are never disturbed by a switch; each
|
|
keeps the profile it started with.
|
|
|
|
## A proxy per account
|
|
|
|
A Headroom proxy keeps what it sees: a cache, a retrieval store and logs, all
|
|
under one workspace directory. One proxy for two accounts would put the work of
|
|
both in one place, which is the thing keeping two accounts is meant to avoid.
|
|
So every account gets its own port, its own workspace and its own user service,
|
|
written from one systemd template:
|
|
|
|
| Address | What |
|
|
| --- | --- |
|
|
| `headroom.localhost` | the proxy of the account that is active right now |
|
|
| `<account>.headroom.localhost` | one account's proxy, active or not |
|
|
| `<jail>.headroom.localhost` | the proxy a jail started for one session |
|
|
|
|
The bare name follows the switch. That is the whole point of it: one address in
|
|
a bookmark, always showing the account being worked in.
|
|
|
|
Jails keep their own names. They are published as run files by the `enqs-jail`
|
|
plugin, and the router here reads the same directory in the same format, so a
|
|
jail is reachable whichever of the two routers is serving the domain -- but
|
|
only one of them can hold the port. `claude-account adopt` stops the plugin's
|
|
router and the hand-written proxy services, and takes their ports and
|
|
workspaces over unchanged.
|
|
|
|
## Commands
|
|
|
|
| Command | What |
|
|
| --- | --- |
|
|
| `status [-v]` | accounts, proxies, names, sessions, and a list of what to fix |
|
|
| `doctor` | the same, and a non-zero exit when something is wrong |
|
|
| `list`, `current`, `dir`, `env`, `url` | the small answers, for scripts |
|
|
| `switch <account>` | make an account the one `claude` runs as |
|
|
| `setup` | wrapper, proxies and router in one go |
|
|
| `adopt` | take over services that were written by hand |
|
|
| `shim install \| uninstall` | the `claude` wrapper |
|
|
| `headroom install \| start \| stop \| restart \| port \| logs` | one account's proxy |
|
|
| `router serve \| install \| uninstall \| port \| list` | the name router |
|
|
|
|
## Configuration
|
|
|
|
`~/.config/claude-account/config.json`. Everything in it has a default derived
|
|
from the machine, so the file is an override and not a requirement:
|
|
|
|
```json
|
|
{
|
|
"active": "work",
|
|
"router_port": 80,
|
|
"accounts": {
|
|
"personal": { "dir": "~/.claude", "port": 8787, "workspace": "~/.headroom" },
|
|
"work": { "dir": "~/.claude-work", "port": 8788, "workspace": "~/.headroom-work" }
|
|
}
|
|
}
|
|
```
|
|
|
|
An account is discovered from `~/.claude` (called `personal`) and every
|
|
`~/.claude-<name>` that holds a login. Ports come from the profile's own
|
|
`settings.json` where it names one, and are handed out from 8790 upwards where
|
|
it does not.
|
|
|
|
The router wants port 80, because that is what makes the address
|
|
`headroom.localhost` rather than `headroom.localhost:8799`. A user service may
|
|
only take it once the machine allows unprivileged low ports:
|
|
|
|
```sh
|
|
sudo sysctl -w net.ipv4.ip_unprivileged_port_start=80 # and in /etc/sysctl.d
|
|
claude-account router port 8799 # or move the router
|
|
```
|
|
|
|
## Requirements
|
|
|
|
* Rust 1.82 to build.
|
|
* systemd as a user session, which keeps the proxies and the router running.
|
|
* `headroom` with the proxy extras: `pip install "headroom-ai[proxy]"`, or a
|
|
path in `CLAUDE_ACCOUNT_HEADROOM_BIN`.
|