Skip to content
Merged
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
23 changes: 23 additions & 0 deletions docs/REGISTRY.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,29 @@ Two traps this catches:
default, so a new `10.43.0.0/16` server reports **zero tools while the hosted
ones work**. That is the block, not the NetworkPolicy. `hermes doctor` first.

## Adding a Claude profile

```yaml
claude_profiles:
- name: work
primary: true
config_dir: "${CLAUDE_CONFIG_DIR:-$HOME/.claude}"
- name: personal
config_dir: "$HOME/.claude-personal"
shares_from: work
shared_paths: [skills, agents, commands, hooks, plugins, settings.json]
```

Exactly one profile is `primary:`, and it keeps the default location so a bare
`claude` needs nothing remembered. Every other profile names the surfaces it
shares, and setup symlinks each one back into the primary.

`shared_paths:` may not name `projects`, `history.jsonl`, `sessions`,
`.claude.json` or `.credentials.json`. Those are the profile's own state, and
sharing them merges the histories the second profile exists to keep apart —
the renderer rejects the edit rather than trusting it. See
[SETUP.md](SETUP.md#two-claude-logins-one-setup).

## Adding a plugin or language server

Plugins go under `plugins:` and must name a marketplace that `marketplaces:`
Expand Down
62 changes: 62 additions & 0 deletions docs/SETUP.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ plugins drive, and the MCP fleet.
uv run python scripts/render_registry.py --check # artifacts current?
./installer/setup-workstation.sh --check # what would change
./installer/setup-workstation.sh # do it
./installer/setup-workstation.sh --no-profiles # primary Claude profile only
```

`setup-workstation.sh` is **generated** from
Expand Down Expand Up @@ -87,6 +88,67 @@ upstream should not abandon the other twenty steps. It counts instead.
The last line is the tally. A zero exit with warnings is a normal outcome on a
machine that does not do Ruby or C#.

## Two Claude logins, one setup

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

The registry declares the profiles under `claude_profiles:`. Today:

| Profile | Config root | Holds |
|---|---|---|
| `work` (primary) | `~/.claude` | the default a bare `claude` uses, and every shared asset |
| `personal` | `~/.claude-personal` | its own login and its own history; everything else is a symlink |

Setup creates the secondary root and symlinks each surface named in its
`shared_paths:` back into the primary — today `skills`, `agents`, `commands`,
`hooks`, `plugins` and `settings.json`. So:

- **Plugins install once.** `plugins/` is shared and `enabledPlugins` lives in
the shared `settings.json`, so a plugin installed for work is already
installed, and already enabled, for personal.
- **MCP servers are registered twice.** A server lives in the profile's own
`.claude.json`, which is exactly the file the two profiles must not share.
Every `claude mcp add` in the generated script runs once per profile.
- **Nothing stateful is shared.** The renderer refuses a `shared_paths:` entry
naming `projects`, `history.jsonl`, `sessions`, `.claude.json` or the
credentials file, and a test proves the refusal fires.

Setup also writes a launcher per secondary profile into `~/.local/bin` (or
`$CLAUDE_LAUNCHER_DIR`), the directory `claude` itself installs into:

```bash
claude-personal # first run: log in, personal account
```

The launcher only sets `CLAUDE_CONFIG_DIR` and execs `claude`. A
`claude-<profile>` file that setup did not write is left alone and reported.

Run setup from a shell **without** `CLAUDE_CONFIG_DIR` set, not from inside a
`claude-personal` session. With it set, the personal root looks like the
primary; setup refuses to link a root onto itself and fails that profile.

A real file or directory already sitting where a symlink would go is **left
alone** and reported in the summary; the script never replaces one. Fix it by
moving your own copy aside and re-running.

### The one thing to verify on macOS

Claude Code stores the OAuth tokens in the login keychain under the service
`Claude Code-credentials`. Whether that entry is namespaced per config root
decides whether two profiles can hold two accounts at once. Check after
logging both in:

```bash
security dump-keychain | grep -c '"svce"<blob>="Claude Code-credentials"'
```

Two entries: the profiles are independent. One: the logins overwrite each
other and a switch means logging in again — use a separate macOS user account
for the personal profile instead.

## Language servers have two halves

A `*-lsp` plugin declares the LSP wiring; the language server **binary** does
Expand Down
Loading
Loading