feat(multi-account): codev account CLI + global-default (Batch 2a + 2e)#123
Conversation
Add a `codev account` CLI that manages ~/.config/codev/accounts.json and regenerates accounts.sh from ONE shared generator, replacing Batch 1's hand-editing. Run in dev via `yarn account <cmd>`. - src/cli/account-manager.ts: shared registry read/write + accounts.sh generator (the 2b Settings UI will reuse the same generator) - src/cli/codev-account.ts: CLI — list/add/default/remove/regenerate/ show/install/uninstall - src/cli/account-manager.test.ts: Vitest suite for the generator (default -> no env, extra -> CLAUDE_CONFIG_DIR, dispatcher * -> defaultAccount, whoami mapping, bash -n validity) - Replace the placeholder CI with a real test workflow (yarn test) - Bump 1.0.77 + CHANGELOG + design-doc §6.A/§7 Verified: generated accounts.sh is functionally equivalent to the hand-written one (diff) and passes `zsh -n`; 9/9 tests green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017kCK4t5JSVynVZxGcrTGGp
buildResumeCommand now emits `command claude` (bypassing the accounts.sh `claude` dispatcher) so a configured non-anchor global-default can't re-route an anchor-account resume to the wrong account. Behavior is unchanged when the default IS the anchor; a no-op for single-account users (command claude == claude). 2e's shell + CLI side already shipped in 2a (`codev account default` plus the dispatcher `*)` -> defaultAccount). Also documents the 2c <-> Batch 3 coupling (cross-account resume needs shared session data). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017kCK4t5JSVynVZxGcrTGGp
📝 WalkthroughWalkthroughAdds shared account-management logic and a ChangesMulti-account management feature
Estimated code review effort: 3 (Moderate) | ~30 minutes Sequence Diagram(s)sequenceDiagram
participant codev_account as codev account
participant account_manager as account-manager
participant accounts_json as accounts.json
participant accounts_sh as accounts.sh
participant zshrc as ~/.zshrc
codev_account->>account_manager: add/remove/default/list/install/uninstall
account_manager->>accounts_json: readRegistry/writeRegistry
account_manager->>accounts_sh: generateAccountsSh/regenerate
codev_account->>account_manager: installShellHook/uninstallShellHook
account_manager->>zshrc: append or remove guarded source block
Possibly related PRs
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 5
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/claude-session-utility.ts`:
- Around line 1081-1093: The resume command builder is interpolating
CLAUDE_CONFIG_DIR without escaping, so paths containing quotes can break the
shell command. Update buildResumeCommand to shell-escape the configDir value
returned by getResumeConfigDirEnv before placing it into the single-quoted
prefix, keeping the rest of the command assembly unchanged.
In `@src/cli/account-manager.ts`:
- Around line 155-210: The generated shell snippet in account-manager’s
formatter block has Prettier violations in the repeated L.push() assembly for
the claude launcher script. Update the code in this section to match Prettier’s
formatting expectations, especially around array-like argument lists and
trailing commas in multiline constructs, so the generated content passes
prettier/prettier checks. Use the L.push sequence that builds the claude,
claude-whoami, and claude-accounts functions as the place to apply the
formatting cleanup.
- Around line 94-102: `readRegistry()` is initializing a new registry with a
hardcoded defaultAccount of 'personal', which prevents `addAccount()` from
selecting the first added account as the default and can leave stale defaults in
place. Update `readRegistry()` so a missing registry starts without any default
account, and ensure `addAccount()` remains responsible for setting the default
only when none exists; use the existing `Registry` shape and `defaultAccount`
handling in `readRegistry` and `addAccount` to locate the change.
- Around line 142-145: The `opts.dir` value is being written into `accounts.sh`
and later sourced, so it must not be interpolated raw into shell content. Update
the account generation flow in `launchPrefix` and the
`claude-whoami`/`claude-accounts` builders to either reject unsafe `--dir`
values up front or escape them with strict shell-safe quoting before persisting.
Make sure the fix covers any path that can contain quotes or shell expansion
characters so the generated wrapper and case entries cannot be broken or
evaluated unexpectedly.
In `@src/cli/codev-account.ts`:
- Around line 73-83: The add command in codev-account.ts is picking the wrong
account label when --dir appears before the label because the label scan
includes the directory value; update the label selection logic in the add case
so it ignores the argument immediately following --dir while still using
getFlag(rest, '--dir') for the directory. Keep the fix local to the add branch
and verify manager.addAccount receives the actual label token (for example, the
work name) instead of the path.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 96ee50bd-1912-4983-be6b-c251cde9bd3f
⛔ Files ignored due to path filters (1)
yarn.lockis excluded by!**/yarn.lock,!**/*.lock
📒 Files selected for processing (9)
.github/workflows/test_ci.yamlCHANGELOG.mddocs/multi-account-support-design.mdpackage.jsonsrc/claude-session-utility.tssrc/cli/account-manager.test.tssrc/cli/account-manager.tssrc/cli/codev-account.tsvitest.config.ts
There was a problem hiding this comment.
7 issues found across 10 files
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
- add: skip the --dir value when picking the label, so `add --dir /foo work` registers `work` (was misread as `/foo`) - shell safety: assertSafeDir rejects shell-metachar dirs at add time AND at generation, so a crafted --dir / hand-edited registry can't inject code into the sourced accounts.sh - default account: launch via `env -u CLAUDE_CONFIG_DIR claude` so a stray exported CLAUDE_CONFIG_DIR can't hijack it; extra accounts use `env CLAUDE_CONFIG_DIR=... claude` (env also bypasses the dispatcher) - resume: single-quote-escape configDir so an apostrophe in the path can't break buildResumeCommand - registry: drop the hardcoded defaultAccount='personal'; set it only when it doesn't name a real account (no dangling reference) - prettier-format the new cli files; +1 injection-rejection test (10/10) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017kCK4t5JSVynVZxGcrTGGp
There was a problem hiding this comment.
1 issue found across 4 files (changes from recent commits).
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="src/cli/account-manager.ts">
<violation number="1" location="src/cli/account-manager.ts:172">
P1: Sourcing generated accounts.sh is still injectable via `configDirEnv` from `accounts.json`. The new guard validates `a.dir` only, so validating `a.configDirEnv` before rendering would close this path.</violation>
</file>
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
The dir guard didn't cover configDirEnv, which launchCmd embeds as the launcher's CLAUDE_CONFIG_DIR — a hand-edited registry could pass a clean dir but an injectable configDirEnv. Validate both at generation. (cubic P1 follow-up.) +1 test. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017kCK4t5JSVynVZxGcrTGGp
There was a problem hiding this comment.
🧹 Nitpick comments (1)
src/cli/account-manager.test.ts (1)
76-90: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winTest doesn't isolate the
dircheck.Both
dirandconfigDirEnvare set to the same unsafe string, so this test can't distinguish whether the thrown error comes from thedirvalidation or theconfigDirEnvvalidation (which is already covered separately at Line 105-119). Use a cleanconfigDirEnvhere to truly isolate thedircheck.♻️ Proposed fix
it('rejects an account dir with shell-unsafe characters', () => { const bad = reg({ defaultAccount: 'evil', accounts: [ { label: 'evil', dir: '/tmp/$(touch pwned)', identityFile: '/tmp/x/.claude.json', - configDirEnv: '/tmp/$(touch pwned)', + configDirEnv: '/tmp/clean', isDefault: false, }, ], }); expect(() => generateAccountsSh(bad)).toThrow(/Unsafe shell characters/); });🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/cli/account-manager.test.ts` around lines 76 - 90, The generateAccountsSh test for shell-unsafe paths is not isolating the dir validation because the account fixture in account-manager.test.ts sets both dir and configDirEnv to the same unsafe value. Update the test case around generateAccountsSh to keep dir unsafe while using a clean configDirEnv, so the assertion specifically exercises the dir check and does not overlap with the separate configDirEnv validation already covered elsewhere.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@src/cli/account-manager.test.ts`:
- Around line 76-90: The generateAccountsSh test for shell-unsafe paths is not
isolating the dir validation because the account fixture in
account-manager.test.ts sets both dir and configDirEnv to the same unsafe value.
Update the test case around generateAccountsSh to keep dir unsafe while using a
clean configDirEnv, so the assertion specifically exercises the dir check and
does not overlap with the separate configDirEnv validation already covered
elsewhere.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 7458d333-fe41-4a24-97e7-54d99e40ed3f
📒 Files selected for processing (4)
src/claude-session-utility.tssrc/cli/account-manager.test.tssrc/cli/account-manager.tssrc/cli/codev-account.ts
🚧 Files skipped from review as they are similar to previous changes (3)
- src/claude-session-utility.ts
- src/cli/codev-account.ts
- src/cli/account-manager.ts
There was a problem hiding this comment.
1 issue found across 2 files (changes from recent commits).
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="src/cli/account-manager.ts">
<violation number="1" location="src/cli/account-manager.ts:177">
P2: A hand-edited accounts.json with a non-string `configDirEnv` can still crash `regenerate` after this validation step. Since this path is explicitly defending against edited registries, it would be safer to reject non-string or empty `configDirEnv` values before calling `expandHome`/`assertSafeDir`.</violation>
</file>
Tip: Review your code locally with the cubic CLI to iterate faster.
Re-trigger cubic
A hand-edited accounts.json with a non-string or empty configDirEnv slipped past the truthy guard and would crash toShellPath. assertSafeDir now rejects non-string/empty values, and the loop validates every non-null configDirEnv. (cubic P2 follow-up.) +1 test. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017kCK4t5JSVynVZxGcrTGGp
whoami read the ambient CLAUDE_CONFIG_DIR (unset -> anchor), but bare `claude` always routes through the dispatcher `*)` to the configured default. So after `default work`, whoami wrongly showed personal. Now it reports the default account and runs auth status under that account's env. +test. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017kCK4t5JSVynVZxGcrTGGp
Inside a Claude Code session, Bash/! commands source the shell snapshot (functions included), so bare `claude` hits the accounts.sh dispatcher and routes to the global default — not the session's account. Document the valid probes (command claude / echo CLAUDE_CONFIG_DIR). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017kCK4t5JSVynVZxGcrTGGp
- generateAccountsSh validates labels (LABEL_RE) alongside dirs — labels are embedded as function names and case patterns, and add's check is bypassable by hand-editing the registry - removeAccount returns the new default only when the removal actually reassigned it, so `remove` of a non-default account no longer prints a misleading 'default account is now X' - docs §5: per-key settings copy note (statusLine et al.) +1 test (13 total) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017kCK4t5JSVynVZxGcrTGGp
Probed a work-account -p session: the symlinked skills/ entry appeared in its skills list and the symlinked global CLAUDE.md's rules were in context. Batch 3's remaining work is UX, not feasibility. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017kCK4t5JSVynVZxGcrTGGp
Batch 3 share UX: never silently overwrite (.codev-bak or skip); link/copy/skip per item; symlink-to-dir works (hard links don't) — per-entry = selective + private skills but new skills need a new link, whole-dir = zero-maintenance auto-share; plugins untested (enabledPlugins lives in per-account settings); session data stays unshared by design — 2c cross-account resume = copy-fork. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017kCK4t5JSVynVZxGcrTGGp
Summary
Batch 2a + 2e of multi-account support (follows #122 / Batch 1).
codev accountCLI: manage the account registry + shell integration from the terminal, replacing Batch 1's hand-editing of~/.config/codev/accounts.json+accounts.sh.codev account default <label>makes bareclauderesolve to a chosen account; CodeV resume now bypasses the shell dispatcher so it can't be misrouted.test_ci.yamlwas a placeholder).What's included
src/cli/account-manager.ts— the single shared generator: reads/writesaccounts.json, generatesaccounts.sh(per-account launchers +claude <account>dispatcher +claude-whoami+claude-accounts). The Batch 2b Settings UI will reuse this same module (no second generator).src/cli/codev-account.ts— the CLI entry.src/cli/account-manager.test.ts— Vitest suite (13 tests).vitest.config.ts,.github/workflows/test_ci.yaml(real CI),package.json(yarn account,yarn test).src/claude-session-utility.ts— 2e:buildResumeCommandemitscommand claude.How to use
codev accountCLI (all commands)Run in dev via
yarn account <cmd>(a realcodevbinary on PATH lands in Batch 2b):After
add/default/remove/regenerate: open a new shell orsource ~/.zshrc.Generated shell commands (what accounts.sh gives you)
Launchers use
envso the default account runs withCLAUDE_CONFIG_DIRexplicitlyunset (
env -u) and extra accounts with it explicitly set — a stray exportedCLAUDE_CONFIG_DIRcan't hijack either.Gotcha: checking the account from inside a Claude Code session
Claude Code snapshots your interactive shell — functions included — and sources it
for Bash-tool /
!commands. So inside any session, bareclaudeis the accounts.shdispatcher function, not the binary:
!claude auth status→authmatches no account label → dispatcher*)→ theglobal default account. It reports the default's identity no matter which account
the session itself runs under — a misleading probe.
!command claude auth status✅ →commandbypasses shell functions → the realbinary inherits the session process's own
CLAUDE_CONFIG_DIR→ reports the session'sactual account.
!echo ${CLAUDE_CONFIG_DIR:-unset}✅ → the session's own config dir (unset= theanchor/default account).
Same rule for scripting: nested
claudeinvocations from inside a session go throughthe dispatcher to the global default — use
command claudeor an explicitclaude <label>when the account matters. (Documented in design doc §6.A.)Cross-account sharing (Batch 3 preview — mechanism verified)
Verified while building this PR (full policy in design doc §5):
CLAUDE.mdskills/commands/(legacy custom slash commands — functionally unified with skills)statusLinesettings.jsonkey → per-key copy, not a symlinkworkplugins/enabledPluginslives in each account'ssettings.json(+ plugin state).claude.jsonhistory.jsonl,projects/,sessions/)Sharing never silently overwrites: existing target →
.codev-bakbackup or skip (§5 collision policy).How to test
Automated:
yarn test(13/13). CI runs it on PRs tomain.Manual smoke-test (needs
yarn make— 2e is a main-process change):yarn account list,yarn account show.yarn account showoutput ≈ your currentaccounts.sh(functionally equal — verified via diff +zsh -n).yarn account default work(sets bareclaude→ work),source ~/.zshrc.command claudefix guarantees this.yarn account default personalwhen done.Verification done
accounts.shis functionally equivalent to the hand-written Batch 1 file (diff) and passeszsh -n.tsc --noEmit: 0 errors.yarn test: 13/13.command claudeis behavior-unchanged when the default is the anchor, and a no-op for single-account users.default work: CLI add/list/show/remove round-trip; CodeV resume of a personal session stayed personal (transcript loaded from~/.claude/projectsproves the account), work sessions resume under work.Notes
accounts.sh— runyarn account regeneratewhen you want the generated version.🤖 Generated with Claude Code
Summary by cubic
Adds the
codev accountCLI with a single shared generator for multi-account setup, plus a configurable global default for bareclaude. Shell integration is hardened, resume usescommand claude, and docs now confirm symlink-based sharing of global files works for Batch 3.New Features
~/.config/codev/accounts.jsonand generatesaccounts.sh(to be reused by the Settings UI).list,add <label> [--dir D],default <label>,remove <label>,regenerate,show,install,uninstall(run in dev withyarn account <cmd>). Global default viacodev account default <label>; dispatcher fallback honors it.yarn test) added.Bug Fixes
add --dirparsing; validate labels and reject shell-unsafe or invalid paths indirandconfigDirEnvat add time and duringaccounts.shgeneration.env -u CLAUDE_CONFIG_DIR claude; others viaenv CLAUDE_CONFIG_DIR=… claude(bypasses the dispatcher).command claudeand escapes single quotes in paths. Dropped hardcodeddefaultAccountand only set it when valid.claude-whoaminow reports the dispatcher default and runsauth statusunder that account’s env. Docs clarify that inside Claude Code sessions, bareclauderoutes to the dispatcher default; usecommand claudeto target the session account.Written for commit 4f030b3. Summary will update on new commits.
Summary by CodeRabbit
codev accountcommand to manage multi-account setup (list, add, set default, remove, regenerate, preview, install/uninstall shell hooks).