Claude Profile Manager: Multiple Accounts, One Machine

Claude Profile Manager: Multiple Accounts, One Machine

Personal subscription, company team, Vertex AI setup — and Claude Code stores everything in one directory. I built a CLI tool that gives each account its own isolated environment.

Jakub Kontra
Jakub Kontra
Developer

Summary

This bugged me for about a month before I decided to fix it. Claude Code stores everything — credentials, settings, MCP servers, project data — in a single ~/.claude directory. No profiles, no multi-account support. Got a personal subscription and a company team? Log out, log in, hope for the best. So I wrote cpm — a simple CLI that gives each account its own claude-<name> command and gets out of the way.


What Made Me Build This

I have a personal Claude subscription for side projects and a company team account for work. Plus I occasionally test things through Vertex AI. Every time I needed to switch, I had to log out, log back in, and then discover that some settings got overwritten. Running two accounts side by side? Not a thing.

One time I accidentally ran the work Claude on a personal project. Wrong billing, wrong credentials — just an annoyance that shouldn't have happened.

For a while I was manually creating directories and setting CLAUDE_CONFIG_DIR, but that got unmaintainable fast. I'd forget to sync MCP servers, break a symlink, once I overwrote my settings entirely. So I automated it.

How It Works

The whole idea is straightforward — cpm creates an isolated directory for each profile and generates a wrapper script that points Claude Code at it.

# Install
brew install jakubkontra/tap/cpm

# Setup wizard — asks about profiles, models, env vars
cpm init

# Creates directories and wrapper scripts
cpm install

# Now you have separate commands
claude-personal    # opens OAuth for personal account
claude-work        # completely independent OAuth for work

After the first cpm install, you've got bash scripts in ~/.local/bin/ for each profile. They work independently of cpm — even if you uninstall it, the wrapper scripts keep running.

Configuration

Everything is driven by a single TOML file:

[profiles.personal]
description = "Personal subscription"
default_model = "opus"

[profiles.work]
description = "Company team account"
default_model = "sonnet"
extra_directories = ["~/Work/shared-configs"]

[profiles.vertex]
description = "Vertex AI (GCP)"
default_model = "sonnet"

[profiles.vertex.env]
CLAUDE_CODE_USE_VERTEX = "1"
CLOUD_ML_REGION = "europe-west1"
ANTHROPIC_VERTEX_PROJECT_ID = "my-gcp-project"

What Gets Shared and What Doesn't

This is probably what I spent the most time thinking about. I ended up with a hybrid strategy:

Copied filessettings.json, settings.local.json, CLAUDE.md. These get copied from ~/.claude because each profile might need different ones. Work profile with different rules than personal — that makes sense.

Symlinked directoriescommands/, skills/, agents/, plugins/. These point back to ~/.claude/. When you add a new custom command, all profiles see it instantly. I didn't want these to diverge.

Credentials — each profile handles its own. cpm deliberately never copies or shares them.

What Wrapper Scripts Actually Do

# Simplified version of claude-work:
unset CLAUDE_CONFIG_DIR CLAUDE_PROFILE ANTHROPIC_API_KEY  # clean slate
export CLAUDE_CONFIG_DIR="$HOME/.claude-profiles/work"
export CLAUDE_PROFILE="work"
exec claude "$@"

That unset at the top matters more than you'd think. Without it, an exported ANTHROPIC_API_KEY in your shell silently leaks into every profile. With a Vertex AI setup, that means auth failures that are incredibly painful to debug.

Per-Project Auto-Switching

This is probably my favorite feature. Just drop a .claude-profile file in any project:

cd ~/Work/company-project
cpm link work
# creates .claude-profile containing "work"

Add the hook to your .zshrc:

echo 'eval "$(cpm hook)"' >> ~/.zshrc

Now every time you cd into that project, the right profile activates automatically. The hook walks up the directory tree — same principle as .nvmrc or .node-version. No thinking, no manual switching.

When Things Break

I added doctor to cpm because I needed a way to quickly check if everything's healthy:

$ cpm doctor
  claude binary found at /usr/local/bin/claude
  source directory ~/.claude exists
  bin directory ~/.local/bin exists and is on PATH
  profile "personal": all symlinks intact
  profile "work": all symlinks intact
  profile "personal": credentials valid (expires in 12 days)
  profile "work": credentials expired 2 days ago

It checks symlinks, credentials, whether the bin directory is on PATH — everything that can go wrong over time. It also watches for OAuth token expiry, which is something I used to forget about constantly.

And cpm status shows where profiles have drifted from the source:

$ cpm status
personal: settings.json diverged from source
work: in sync
vertex: CLAUDE.md diverged from source

If you want to bring profiles back in sync, cpm install --sync re-copies mutable files from ~/.claude.

Things I Ran Into (So You Don't Have To)

A few things that cost me time and that cpm handles for you:

  1. Manual directory management. Works until you forget to sync MCP servers. Or break a symlink. Or a new shared directory gets added. It just doesn't scale.

  2. Sharing credentials. Each profile must authenticate on its own. cpm deliberately doesn't copy credentials — OAuth is OAuth, there are no shortcuts.

  3. Env var leakage. An ANTHROPIC_API_KEY in your shell silently bleeds into everything. Wrapper scripts fix this with unset at the top.

  4. Duplicating skills and commands. You don't need them separate in every profile. Symlinks work great — one copy, shared everywhere.

  5. Expired tokens. OAuth tokens expire and you find out mid-session. cpm doctor warns you ahead of time.

Technical Details

cpm is written in Go with two dependencies — cobra for the CLI and BurntSushi/toml for config parsing. Static binary, no runtime dependencies.

The whole codebase is around a thousand lines. It's not a complex tool — it solves one specific problem and tries to solve it well. Wrapper scripts are plain bash, work without cpm on PATH, exec replaces the shell process with zero overhead.

FAQ

Why bash wrapper scripts and not a Go shim?

Because I want zero runtime dependency. Once cpm install generates the scripts, they work without cpm. A shim binary would add latency and be a single point of failure. A bash script is simple, readable, debuggable.

Does it work with Claude Code in IDEs?

The wrapper scripts work in any terminal. For VS Code or JetBrains, set CLAUDE_CONFIG_DIR in your IDE's environment settings — point it at the profile directory.

Can I share config across machines?

config.toml is portable. But profile directories contain machine-specific symlinks and credentials, so you need to run cpm install on each machine.

How does it handle MCP servers?

On each cpm install, it reads the global ~/.claude.json and copies the mcpServers block into each profile's config. All profiles get the same MCP servers automatically.


The project is on GitHub. Install with brew install jakubkontra/tap/cpm. If you're dealing with a similar problem, give it a shot — and feel free to open an issue if something doesn't work.