diff --git a/README.md b/README.md index 574636d..b75dd5c 100644 --- a/README.md +++ b/README.md @@ -137,6 +137,8 @@ Run multiple Claude Code accounts (e.g. personal + work) on one machine. Each ac Tab completion included (zsh): `claude ` completes account names; `codev account ` 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-` 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 `, or `claude-` (the UI's row shows this). **`codev account` CLI** (the account manager — same generator the Settings UI uses): @@ -155,6 +157,7 @@ The **default account** can be launched three equivalent ways: bare `claude`, `c | `codev account share --link\|--copy [--entry E]` | Share the anchor's item — link stays in sync, copy forks | | `codev account unshare [--entry E] [--restore-backup\|--keep-copy]` | Remove the link; `--restore-backup` = true undo, `--keep-copy` = keep a fork | | `codev account sync-settings ` | Copy settings keys from the anchor (`statusLine`, `model`, `effortLevel`, `theme`) | +| `codev account share-memory 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 ` registers *name → config folder* (default `~/.claude-` — that folder **is** the account's `CLAUDE_CONFIG_DIR`). Claude Code itself creates and fills the folder on first login (`claude `). One folder = one account: registering an already-registered folder under a second name is rejected. > @@ -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 --dir ` — **any folder works**; `~/.claude-` 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//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 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 ` and `claude-` — 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). diff --git a/docs/multi-account-support-design.md b/docs/multi-account-support-design.md index c5cd33f..55f847f 100644 --- a/docs/multi-account-support-design.md +++ b/docs/multi-account-support-design.md @@ -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//SKILL.md` and `commands/.md` +are found only under `/skills/` and `/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////`) 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: + +- **`/hooks/` and `/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/`. +- **`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 + `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` (`/CLAUDE.md`) | Project folder | **Yes, automatically** | Lives in the repo; account-independent |