Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 25 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,8 @@ Run multiple Claude Code accounts (e.g. personal + work) on one machine. Each ac

Tab completion included (zsh): `claude <TAB>` completes account names; `codev account <TAB>` completes subcommands and labels. (The `claude` completion is registered only when nothing else completes `claude` — if you already have one, account names won't be injected into it.)

> **In cmux, use the `claude-<name>` form.** cmux's own shell integration installs a `claude` wrapper *after* `~/.zshrc` is read, which replaces this dispatcher — so `claude work` there does not switch accounts; it hands `work` to Claude Code as the initial prompt and starts the session under the anchor, with no error ([#161](https://github.com/grimmerk/codev/issues/161)). `claude-work` is untouched and works, and so does CodeV's own account picker, which never goes through the dispatcher.

The **default account** can be launched three equivalent ways: bare `claude`, `claude <its-name>`, or `claude-<its-name>` (the UI's row shows this).

**`codev account` CLI** (the account manager — same generator the Settings UI uses):
Expand All @@ -155,6 +157,7 @@ The **default account** can be launched three equivalent ways: bare `claude`, `c
| `codev account share <name> <item> --link\|--copy [--entry E]` | Share the anchor's item — link stays in sync, copy forks |
| `codev account unshare <name> <item> [--entry E] [--restore-backup\|--keep-copy]` | Remove the link; `--restore-backup` = true undo, `--keep-copy` = keep a fork |
| `codev account sync-settings <name> <key...>` | Copy settings keys from the anchor (`statusLine`, `model`, `effortLevel`, `theme`) |
| `codev account share-memory <name> on\|off` | Point that account's auto-memory at the anchor's, per project (see **Shared auto-memory** below) |

> **Add is just a mapping.** `add <name>` registers *name → config folder* (default `~/.claude-<name>` — that folder **is** the account's `CLAUDE_CONFIG_DIR`). Claude Code itself creates and fills the folder on first login (`claude <name>`). One folder = one account: registering an already-registered folder under a second name is rejected.
>
Expand All @@ -172,28 +175,46 @@ The **default account** can be launched three equivalent ways: bare `claude`, `c
| Already running a second account by hand (own shell function + custom folder) | Register it with `codev account add <name> --dir <your-folder>` — **any folder works**; `~/.claude-<name>` is only the default. Identity and sessions attach immediately, no re-login. If your hand-rolled wrapper was named `claude`, retire it (it would fight the generated dispatcher). |
| Wondering why a name maps to a different folder | Renames change only the name side of the *name → folder* mapping; folders never move (credentials are keyed by the folder path) — the UI shows `· folder: …` on the row when they diverge. |

> Not yet supported: auto-detecting existing config folders for one-click registration, and a warning when `accounts.sh` overrides a hand-rolled `claude()` shell function ([#127](https://github.com/grimmerk/codev/issues/127)); continuing an existing conversation under a *different* account — "copy-fork" — needs transcript-level workarounds first ([#128](https://github.com/grimmerk/codev/issues/128)).
> Not yet supported: auto-detecting existing config folders for one-click registration, and a warning when `accounts.sh` overrides a hand-rolled `claude()` shell function ([#127](https://github.com/grimmerk/codev/issues/127)) — or when something else overrides *it*, which is what cmux does ([#161](https://github.com/grimmerk/codev/issues/161)); continuing an existing conversation under a *different* account — "copy-fork" — needs transcript-level workarounds first ([#128](https://github.com/grimmerk/codev/issues/128)).

The `codev` command runs the CLI **bundled inside CodeV.app** (`ELECTRON_RUN_AS_NODE`) — no system Node, no sudo, no PATH edits. A **packaged** CodeV refreshes `accounts.sh` from its own bundled generator on every launch, so moving or renaming the app self-heals. (Not available in MAS builds — sandboxed.)

The `codev` command runs the CLI **bundled inside CodeV.app** (`ELECTRON_RUN_AS_NODE`) — no system Node, no sudo, no PATH edits. CodeV refreshes `accounts.sh` on every launch, so moving or renaming the app self-heals. (Not available in MAS builds — sandboxed.)
> **The refresh is packaged-only, and an older app downgrades the file.** `yarn start` skips it entirely (the sync returns early unless `app.isPackaged`), so a development run never rewrites `accounts.sh`. The flip side is that the *last thing to run* decides the file's contents: regenerate from a newer checkout with `yarn account regenerate`, then launch an older installed CodeV, and the older app's template silently replaces it — the registry still says what you configured, but the generated functions are the old ones. Install the matching build rather than relying on a hand-run regenerate.

**Cross-account sharing** (also in the UI: each account row's **Sharing** button):

Share the anchor's global files with other accounts — per item, three choices: **Link** (symlink; one file, stays in sync, edits from either side land in the same place), **Copy** (independent fork), or skip. Never silently overwrites: existing content is backed up to a timestamped `.codev-bak-*` sibling first, which also makes **Unlink & restore** a true undo. Plain Unlink loses nothing (the anchor's copy is untouched; re-link anytime). The panel also has one-click **settings-key sync** buttons (`statusLine` / `model` / `effortLevel` / `theme`) and refreshes automatically when the window regains focus (so terminal-side file changes show up live).

The same panel carries one checkbox that is neither Link nor Copy — **Memory: share with the anchor, per project** — because auto-memory is redirected at launch rather than on disk. See **Shared auto-memory** below.

| Item | Shareable? | How |
|------|------------|-----|
| Global `CLAUDE.md`, `skills/`, `commands/` | ✅ | Link or Copy (verified: Claude Code follows symlinks) |
| `statusLine` / `model` / `effortLevel` / `theme` | ✅ | `sync-settings` (per-key copy — they live in `settings.json`) |
| Auto-memory (`projects/<slug>/memory/`) | ✅ | **Not** a symlink — a per-launch `--settings` redirect, per project. See below |
| `plugins/` | ❌ | Per-account install state with absolute paths. Install and enable plugins in each account separately — `enabledPlugins` is per-account and **not** a syncable key |
| `.claude.json`, session data, hooks | ❌ | Identity / live-written / installed per-dir by CodeV |

**Shared auto-memory** (Settings → Accounts → **Sharing** → *Memory: share with the anchor, per project*, or `codev account share-memory <name> on|off`):

Claude Code keeps auto-memory **per project**, under each account's own config dir, so two accounts on one machine keep two separate memories for the same repository. Turning this on points a non-anchor account at the **anchor's** copy of whichever project a session starts in. It is off by default, and the anchor is never offered it — the anchor's memory is the one everyone else shares.

The redirect is computed at launch and passed as `--settings`, so nothing is written into the repository and neither account's `settings.json` is touched. One repository means one memory: a repository, its subdirectories and all of its linked worktrees resolve to the same key (Claude Code keys on the git common directory's parent; outside a repository it is the working directory itself, with symlinks resolved).

| | |
|---|---|
| **Shared** | Auto-memory for the project a session starts in |
| **Not shared** | Session transcripts — Claude Code still writes them under the launching account, which is what keeps CodeV's per-account session attribution working |
| **Carries the redirect** | **Both** generated launcher forms — `claude <name>` and `claude-<name>` — emit the same flag, so memory is shared identically whichever one you use. That matters because cmux forces you onto the second form. Plus CodeV's resume and CodeV's new-session launch under a picked account |
| **Does not** | A session started outside both — a bare `CLAUDE_CONFIG_DIR=… claude`, or the VS Code extension — uses that account's own memory for that session. Nothing breaks; it just does not see the shared one |

**In the CodeV UI:**

| Where | What |
|-------|------|
| Settings → Accounts | List/add/remove/rename accounts, set the global default, install shell integration, per-account **Sharing** panel (link/copy/unlink + settings-key sync) |
| Settings → Accounts | List/add/remove/rename accounts, set the global default, install shell integration, per-account **Sharing** panel (link/copy/unlink, settings-key sync, shared auto-memory) |
| Sessions tab | Sessions from all accounts with account badges; resume uses each session's own account |
| Projects tab: `⌥⌘+Enter` | Pick the account for a new session (`⌘+Enter` stays instant, under the global default). Account override applies to external terminals (iTerm2, Terminal.app, Ghostty, cmux); VS Code ([#121](https://github.com/grimmerk/codev/issues/121)) and the embedded Term tab ignore it |
| Projects tab: `⌥⌘+Enter` | Pick the account for a new session (`⌘+Enter` stays instant, under the global default). Account override applies to external terminals (iTerm2, Terminal.app, Ghostty, cmux — **cmux included**, since CodeV launches with an explicit `CLAUDE_CONFIG_DIR` and never goes through the shell dispatcher, which is what cmux's own `claude` wrapper replaces ([#161](https://github.com/grimmerk/codev/issues/161))); VS Code ([#121](https://github.com/grimmerk/codev/issues/121)) and the embedded Term tab ignore it |

**Gotcha:** inside a Claude Code session, `!claude auth status` reports the *global default* (the shell snapshot carries the dispatcher function), not the session's account — use `!command claude auth status` instead. Full design + details: [docs/multi-account-support-design.md](docs/multi-account-support-design.md).

Expand Down
50 changes: 50 additions & 0 deletions docs/multi-account-support-design.md
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,56 @@ The registry records `configDirEnv` (null for default, the dir for extras) and

## 5. Cross-account file reuse (answering the reuse question)

**Why each row of the table below gets the mechanism it gets.** Whether an item
can be shared, and by what, follows from how Claude Code *locates* it. There are
two ways and one hybrid, and almost every "can we just symlink this?" question is
answered by which one the item falls under.

**It scans a fixed directory.** `skills/<name>/SKILL.md` and `commands/<name>.md`
are found only under `<config dir>/skills/` and `<config dir>/commands/`; nothing
outside those paths exists as far as Claude Code is concerned. So the second
account needs something physically present in *its own* directory, and a symlink
is the only **zero-drift** way to put it there (a periodic copy also satisfies the
scan — it just drifts).

**It runs a path you wrote.** A hook's `command` and `statusLine.command` are
arbitrary paths, so the script needs no sharing mechanism at all: keep one copy
anywhere and register the same absolute path in both accounts' `settings.json`.
What drifts here is the **registration**, not the script — which is why §6.F
installs hooks per dir rather than trying to share a file.

**Directory plus per-account state.** `plugins/` has a payload directory
(`cache/<marketplace>/<plugin>/<version>/`) that could be linked, but the install
registry (`installed_plugins.json`, one absolute `installPath` per entry) and the
enablement (`enabledPlugins`, in `settings.json`) are state, and Claude Code writes
both on every install, update and sweep. Linking the payload saves disk and
nothing else — measured 2026-09-19 on a two-account machine, the duplication was
~780 KB against 9.6 MB and 7.6 MB of `plugins/` — while linking the registries
would make one account's install silently change the other's. The analogy is
`node_modules` plus `package.json`, not `skills/`. A *self-written* plugin needs no
link either: add the same local path as a marketplace in both accounts.

Two consequences worth stating, because both look like conventions and are not:

- **`<config dir>/hooks/` and `<config dir>/scripts/` are not Claude Code
directories.** Measured 2026-09-19: neither exists in a second account Claude
Code has managed for months, while `skills/`, `commands/`, `plugins/`,
`projects/` and `sessions/` exist in both. Claude Code never creates them because
it never looks for them — they are a user's own filing convention, and a hook
command may equally point at `~/.cargo/bin/<tool>`.
- **`settings.json` is a mixed file** — preferences beside identity, security and

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: This rationale misstates settings.json and contradicts the table's optional whole-file symlink mechanism. Describe the four allowlisted keys as per-key copies, and describe whole-file linking as possible but risky; account identity lives in .claude.json.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At docs/multi-account-support-design.md, line 242:

<comment>This rationale misstates `settings.json` and contradicts the table's optional whole-file symlink mechanism. Describe the four allowlisted keys as per-key copies, and describe whole-file linking as possible but risky; account identity lives in `.claude.json`.</comment>

<file context>
@@ -202,6 +202,56 @@ The registry records `configDirEnv` (null for default, the dir for extras) and
+  `projects/` and `sessions/` exist in both. Claude Code never creates them because
+  it never looks for them — they are a user's own filing convention, and a hook
+  command may equally point at `~/.cargo/bin/<tool>`.
+- **`settings.json` is a mixed file** — preferences beside identity, security and
+  machinery — which is why the table shares four keys out of it by copy and nothing
+  by link. A whole-file symlink would carry `permissions`, `hooks` and
</file context>

machinery — which is why the table shares four keys out of it by copy and nothing
by link. A whole-file symlink would carry `permissions`, `hooks` and
`enabledPlugins` across too; Claude Code writes the file itself (`/model`,
`/effort`, theme), so one account's change would silently become the other's; and
CodeV writes into *every* account's copy (§6.F), so a link would have CodeV
believing it wrote one account while writing another. The cost of the per-key
copy is the honest one: it is one-shot and goes stale.

The fourth mechanism in the table — a **launch-time redirect** (§5.1) — exists
because auto-memory fits none of the three: it is one directory per project,
created on demand, and symlinking `memory/` is refused outright.

| File / data | Scope | Shared across accounts? | How |
|---|---|---|---|
| Project `CLAUDE.md` (`<repo>/CLAUDE.md`) | Project folder | **Yes, automatically** | Lives in the repo; account-independent |
Expand Down
Loading