Skip to content

feat(installer): second Claude Code profile and claude-<profile> launcher - #58

Merged
ExtraToast merged 3 commits into
mainfrom
feat/twin-claude-profiles
Sep 15, 2026
Merged

ExtraToast merged 3 commits into
mainfrom
feat/twin-claude-profiles

Conversation

@ExtraToast

Copy link
Copy Markdown
Contributor

Why

One workstation has two Claude Code logins, work and personal. Their conversation histories must stay apart. CLAUDE_CONFIG_DIR moves a whole config root: credentials, projects/, history.jsonl, sessions/ and .claude.json. So a second login needs a second config root. Nothing in the kit created that root. Nothing gave it the skills, plugins or MCP fleet, and nothing gave it a command to start it.

Change

  • New claude_profiles: in registry/estate-tooling.yaml. work is the primary and keeps ~/.claude, so a bare claude works as before. personal lives at ~/.claude-personal.
  • A secondary profile shares the paths in its shared_paths: by symlink back into the primary: skills, agents, commands, hooks, plugins and settings.json.
    • The renderer refuses to share projects, history.jsonl, sessions, .claude.json or the credentials file.
    • Setup never replaces a real file or directory that is already at a link path. It reports it instead.
  • Plugins install once, into the primary. MCP servers are registered once per profile, because each server lives in the profile's own .claude.json.
  • Setup writes a claude-<profile> launcher into ~/.local/bin (or $CLAUDE_LAUNCHER_DIR). It sets CLAUDE_CONFIG_DIR and execs claude.
    • Setup leaves a launcher that it did not write alone.
    • --check writes nothing.
    • Setup warns when the directory is not on PATH.
  • Profile names must be valid command names.
  • --no-profiles skips the secondary profiles.
  • docs/SETUP.md and docs/REGISTRY.md document the profiles.

Bug fixes

  • An unset optional credential aborted the whole run. The script ran under set -u, so --env OVERLEAF_SESSION="${OVERLEAF_SESSION}" exited, and every step after overleaf was skipped. Optional credentials now render as ${VAR:-}.
  • The unknown-MCP-server check used a PCRE lookahead. BSD grep rejects that, so on macOS the check printed an error and inspected nothing. A sed expression replaces it.
  • Setup run from inside a claude-personal session saw ~/.claude-personal as the primary root. It then linked every shared path of that root onto itself, and the profile could no longer load skills, plugins or settings. This happened on a real run. Setup now fails that profile, and link_profile_path refuses to link a root onto itself.

Verification

  • render_registry.py --check passes, 103 tests pass, ruff is clean, shellcheck -S warning is clean, and bash -n passes.
  • The tests drive link_profile_path and install_profile_launcher against a real temporary filesystem:
    • The links are created, and a re-run changes nothing.
    • A real directory at a link path is kept.
    • A root is not linked onto itself.
    • The launcher passes the config root and the arguments to a stub claude.
    • A hand-written launcher is kept.
    • --check writes nothing.
  • On a real workstation:
    • ~/.claude-personal holds six symlinks into ~/.claude and has its own .claude.json with the MCP fleet.
    • claude-personal --version returns 2.1.272 (Claude Code) from a shell without CLAUDE_CONFIG_DIR set.
    • setup-workstation.sh --check, run with CLAUDE_CONFIG_DIR set to the personal root, stops with CLAUDE_CONFIG_DIR points at ... re-run with it unset.
  • Not verified yet: whether the macOS keychain keeps a separate OAuth entry for each config root. docs/SETUP.md describes how to check this after both profiles have logged in.

CLAUDE_CONFIG_DIR moves a Claude Code config root whole: credentials,
projects/, history.jsonl, sessions/ and .claude.json travel together.
That is what makes a second login a second directory, and it is what
keeps a personal conversation out of the work account's history.

Declare the roots in the registry under claude_profiles:. The primary
keeps the default location, so a bare `claude` needs nothing remembered;
every secondary is created by setup and shares the surfaces it names in
shared_paths: by symlinking back into the primary. Plugins install once,
because plugins/ is shared and enabledPlugins lives in the shared
settings.json. MCP servers are registered once per profile, because a
server lives in the profile's own .claude.json - the one file the two
profiles must not share.

The renderer refuses a shared_paths: entry naming projects,
history.jsonl, sessions, .claude.json or the credentials file: sharing
one merges the histories the second profile exists to keep apart.

Two defects found while running the result:

- An unset OPTIONAL credential aborted the whole script. It renders as
  --env VAR="${VAR}", and under `set -u` that is not an empty string,
  it is an exit - so an unexported OVERLEAF_SESSION took every step
  after overleaf with it. Optional credentials now render ${VAR:-}.
- The unknown-server check used a PCRE lookahead, `(?=:)`, which BSD
  grep rejects as "repetition-operator operand invalid". It printed an
  error and inspected nothing on macOS. A sed expression replaces it.
…ofile

A secondary profile was reachable only as
CLAUDE_CONFIG_DIR=$HOME/.claude-personal claude; nothing created a
command for it. Setup now writes ~/.local/bin/claude-<profile> (or
$CLAUDE_LAUNCHER_DIR), which sets the config root and execs claude. A
file at that path that setup did not write is left alone and reported,
--check writes nothing, and a launcher directory missing from PATH is
warned about. Profile names are validated as command names.

Also fix a defect found while running it: setup run from inside a
secondary profile's session sees CLAUDE_CONFIG_DIR as the primary root,
and linked every shared surface of that root onto itself, leaving the
profile unable to load skills, plugins or settings. Setup now fails that
profile instead, and link_profile_path refuses to link a root onto
itself.
@ExtraToast ExtraToast added type: feature New user-facing or operator-facing capability. area: tooling Reusable workflows, Gradle, templates, Renovate, and API tooling. component: config Configuration schema, defaults, or repo settings. labels Sep 15, 2026
@ExtraToast
ExtraToast merged commit ce69717 into main Sep 15, 2026
11 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: tooling Reusable workflows, Gradle, templates, Renovate, and API tooling. component: config Configuration schema, defaults, or repo settings. type: feature New user-facing or operator-facing capability.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant