From 4ca2f0f374017e6f0674340784a3863b41f8b23c Mon Sep 17 00:00:00 2001 From: Grimmer Kang Date: Sat, 19 Sep 2026 03:53:53 +0800 Subject: [PATCH 1/5] docs(readme): shared auto-memory, and two traps it exposed MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #160 shipped without touching the README, so the Multi-Account section still describes sharing as symlink-or-copy only. Adds `share-memory` to the CLI table, an auto-memory row to the shareable-items table, and a short section of its own: what is shared (memory), what is not (transcripts), and which launch paths carry the redirect. The point the table cannot make on its own is that this one is NOT a symlink — it is a per-launch `--settings` redirect — so it needed prose beside the Link/Copy wording. Two corrections while in there, both found the hard way: "CodeV refreshes accounts.sh on every launch" is packaged-only; the sync returns early unless `app.isPackaged`. The consequence is worth stating: regenerate from a newer checkout, then open an older installed CodeV, and the older template silently replaces the file. And cmux's shell integration installs its own `claude` wrapper after ~/.zshrc, replacing the dispatcher — so `claude ` there sends the account name as a prompt and starts under the anchor, silently. `claude-` still works and is now the documented form for cmux (#161). Co-Authored-By: Claude Opus 5 (1M context) --- README.md | 27 ++++++++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 574636d..d199c9a 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,26 +175,44 @@ 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** | `claude ` through the generated `accounts.sh`; CodeV's resume; 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 | From bd83ec8030ec56d6f9ab3a1469bd03bbd8de2baf Mon Sep 17 00:00:00 2001 From: Grimmer Kang Date: Sat, 19 Sep 2026 04:12:02 +0800 Subject: [PATCH 2/5] docs(readme): say the account picker survives cmux The new cmux warning and the UI table read as contradicting each other: one says the dispatcher does not switch accounts in cmux, the other says the account override applies to cmux. Both are true for different launch paths, so the table now says which and why. Co-Authored-By: Claude Opus 5 (1M context) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d199c9a..9777b61 100644 --- a/README.md +++ b/README.md @@ -214,7 +214,7 @@ The redirect is computed at launch and passed as `--settings`, so nothing is wri |-------|------| | 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 [#161](https://github.com/grimmerk/codev/issues/161) breaks); 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). From d8b0a263f3049adc14c45366c85afd250c8c11fe Mon Sep 17 00:00:00 2001 From: Grimmer Kang Date: Sat, 19 Sep 2026 14:41:21 +0800 Subject: [PATCH 3/5] docs(readme): both launcher forms carry the memory redirect MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both reviewers caught the same gap, and it lands exactly where it hurts: the previous round told cmux users to switch to `claude-`, while the shared-memory table listed only `claude ` as carrying the redirect. Verified from the generator rather than assumed — `claude-` and the dispatcher branch both come from launchCmd(account, true), and generating accounts.sh from a sharing registry emits the identical `--settings "$(_codev_memory_settings)"` in both. Also fixes a sentence I broke in the previous commit: a trailing "breaks" left dangling after the #161 link. Co-Authored-By: Claude Opus 5 (1M context) --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 9777b61..a49faf4 100644 --- a/README.md +++ b/README.md @@ -205,7 +205,7 @@ The redirect is computed at launch and passed as `--settings`, so nothing is wri |---|---| | **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** | `claude ` through the generated `accounts.sh`; CodeV's resume; CodeV's new-session launch under a picked account | +| **Carries the redirect** | **Both** generated launcher forms — `claude ` and `claude-` — emit the same flag, so the form cmux forces you onto shares memory exactly like the dispatcher; 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:** @@ -214,7 +214,7 @@ The redirect is computed at launch and passed as `--settings`, so nothing is wri |-------|------| | 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 — **cmux included**, since CodeV launches with an explicit `CLAUDE_CONFIG_DIR` and never goes through the shell dispatcher [#161](https://github.com/grimmerk/codev/issues/161) breaks); 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). From 82a44fc6e6fd0dd4bf008c9e3524e7032ab25916 Mon Sep 17 00:00:00 2001 From: Grimmer Kang Date: Sat, 19 Sep 2026 20:18:26 +0800 Subject: [PATCH 4/5] docs(design): why each shared item gets the mechanism it gets MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Section 5's table says WHAT each item does; it never said why, so every new item was a fresh argument. It follows from how Claude Code locates the thing: it scans a fixed directory (only a symlink is zero-drift), it runs a path you wrote (no sharing mechanism needed at all — one script, two registrations), or it is a directory plus per-account state (plugins: linking the payload saves ~780KB and nothing else, linking the registries makes one account's install change the other's). Two things that look like conventions and are not: hooks/ and scripts/ are NOT Claude Code directories — measured, neither exists in a second account it has managed for months — and settings.json is a mixed file, which is the whole reason four keys are copied out of it rather than the file being linked. Co-Authored-By: Claude Opus 5 (1M context) --- docs/multi-account-support-design.md | 50 ++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) 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 | From 7974fc2c0f13e0c9bcf47c2bdef38bbe5bfee2e4 Mon Sep 17 00:00:00 2001 From: Grimmer Kang Date: Sat, 19 Sep 2026 20:21:17 +0800 Subject: [PATCH 5/5] docs(readme): unambiguous wording in the redirect row Grammatically fine ("the form [that] cmux forces you onto shares memory"), but a reviewer parsed "shares" as a noun, which is evidence enough that a reader would too. Split into two sentences. Co-Authored-By: Claude Opus 5 (1M context) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a49faf4..b75dd5c 100644 --- a/README.md +++ b/README.md @@ -205,7 +205,7 @@ The redirect is computed at launch and passed as `--settings`, so nothing is wri |---|---| | **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 the form cmux forces you onto shares memory exactly like the dispatcher; plus CodeV's resume and CodeV's new-session launch under a picked account | +| **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:**