Run multiple Claude Code accounts in parallel. Fully isolated. One command.
ccpm (Claude Code Profile Manager) lets you keep multiple isolated Claude Code profiles on one machine. Each profile has its own credentials, settings, MCP servers, plugins, skills, and memory. Open two terminals, run two different accounts at the same time.
Claude Code reads its config from a single directory (~/.claude). Without ccpm you can only be signed into one account at a time and switching means logging out, swapping files, or both.
ccpm gives every profile its own config directory and sets CLAUDE_CONFIG_DIR to it when you launch claude. Two terminals, two profiles, zero conflicts.
# npm
npm i -g @ngcodes/ccpm
# go
go install github.com/nitin-1926/claude-code-profile-manager/ccpm@latest
# curl (macOS / Linux)
curl -fsSL https://raw.githubusercontent.com/nitin-1926/claude-code-profile-manager/main/scripts/install.sh | sh
# source
git clone https://github.com/nitin-1926/claude-code-profile-manager.git
cd claude-code-profile-manager/ccpm
go build -o ccpm .
./ccpm --versioncurl -fsSL https://raw.githubusercontent.com/nitin-1926/claude-code-profile-manager/main/scripts/install-desktop.sh | shA native app over the same engine. The installer picks your chip, verifies the checksum and puts CCPM in Applications, and the app updates itself from then on. A downloadable .dmg is coming soon. The app uses the ccpm CLI for changes, so install the CLI too. More in Desktop app.
# Create profiles (each prompts for OAuth or API key)
ccpm add personal
ccpm add work
# Run them in parallel
ccpm run personal # terminal 1
ccpm run work # terminal 2
# See what you have
ccpm listSample output:
NAME AUTH STATUS
personal oauth ✓ nitin@gmail.com
work api_key ✓ sk-ant-...7f2k ★
If you already use Claude Code, your first profile doesn't have to start empty. When ~/.claude exists, the ccpm add wizard offers to import your current config — skills, MCP servers, hooks, agents, commands, rules, and settings all carry over into the new profile.
# the wizard prompts: start empty, import from ~/.claude, or clone a profile
ccpm add personalTo do it explicitly (or to pull in changes you made to ~/.claude later), run ccpm import default — see the Import section for --only, --all, and dry-run flags.
Release notes live on the docs site: ccpm.nitingupta.xyz/changelog.
- Parallel sessions: run different Claude Code accounts side-by-side, fully isolated.
- OAuth + API key per profile, with credentials in the OS keychain.
- Per-profile assets: skills, agents, commands, rules, hooks, plugins, MCP servers, and settings install globally, per-profile, or per-project.
- MCP transports: stdio, HTTP, and SSE. Remote MCPs authenticate via
ccpm mcp authso OAuth tokens land in the right profile. - Permissions and hooks without hand-editing JSON.
- Transparent arg forwarding: unknown flags after
ccpm run <profile>flow through to claude with no--separator. - IDE default:
ccpm set-defaultpins a profile for directclaudelaunches (VS Code, Cursor, Antigravity, any GUI extension). On macOS this is set system-wide via a LaunchAgent. - Encrypted vault: AES-256-GCM credential backups with a master key in your OS keychain.
- Shared store: directory-based assets are symlinked from
~/.ccpm/share/into profiles for deduplication. - macOS is the verified platform today. Linux and Windows builds compile and ship; OAuth
set-default,auth backup/restore, and keychain-basedstatuson those platforms are experimental until verified on real hardware.
A native desktop GUI for managing profiles lives in ccpm/desktop/. It's built with Wails (Go + native webview) in the same module as the CLI — so it reuses ccpm's own engine for reads and shells out to the ccpm CLI for writes (same locking, keychain, and validation). Local-first, no signup.
It gives you a left sidebar of profiles and, per profile, tabs for Overview, Cascade (the effective host→global→profile config with provenance badges and shadow/override hints), Assets, MCP & Plugins, Permissions (rules, mode, env), Settings (settings JSON, the status line layout, and the usage notch), Usage (an amber token-usage dashboard mirroring ccpm usage), History (browse, read and search this profile's past sessions), and Health (ccpm doctor). Clone / rename / delete / open / run profiles from the toolbar; the view auto-refreshes when the CLI changes things underneath it. Creating a profile or importing ~/.claude opens a Terminal running the ccpm add wizard (in-GUI sign-in is on the roadmap).
Overview — every profile's assets at a glance.
Usage — token spend, burn rate, and a live 5-hour block, per profile.
MCP & Plugins — manage servers and toggle plugins per profile.
An optional notch that grows out of a screen edge, with one ring per profile: the big ring is your 5-hour window (or your weekly one, if you pick it as the main ring), the thin inner ring the other, coloured by how much headroom is left — the same thresholds ccpm statusline uses, so the notch and your terminal never disagree. On the top edge of a Mac with a camera notch it joins the camera notch. Hover a ring for the detail: which account and plan it belongs to, how old the reading is, and per window a reset time, a bar, and the percentage used.
Configure it in the Usage notch section of the Settings tab: a switch to show or hide it, and while it is on, whether it opens on hover or stays open, which edge it sits on (Top, Left, Right or Bottom), which window is the main ring, whether to show the percentage under each ring (hiding it gives the rings more room, handy on the top edge with several profiles), and a switch per profile. Turning it off and back on keeps your choices. In hover mode it folds to a slim pill at the screen edge and opens when you reach for it (Reduce Motion turns the animation into a jump).
Where the numbers come from. Claude Code already sends your rate-limit windows to
ccpm statusline, and ccpm caches them per profile. No API call, no credentials read. The trade-off is that a profile's reading only refreshes when you actually use Claude Code on it — so the callout always tells you how old the number is, and says so plainly once it goes stale. A profile you've never run shows an empty ring and "No reading yet" rather than a misleading 0%, and so does a profile with its own customstatusLine, because only ccpm's status line records the readings. Only subscription plans (Max/Pro) report limits at all; API-key profiles say so instead of pretending.
One line in Terminal. It picks the build for your chip, verifies its checksum and puts CCPM in Applications, ready to open:
curl -fsSL https://raw.githubusercontent.com/nitin-1926/claude-code-profile-manager/main/scripts/install-desktop.sh | shA downloadable .dmg is coming soon.
Updates are automatic. When a new version ships, the app shows an in-app Update now prompt, downloads it, and swaps itself in place, with nothing to re-download. If it can't replace itself (its folder isn't writable by you, or macOS is running it from a temporary location because it was never moved to Applications), it tells you instead of quitting. The desktop app versions independently of the CLI (released on desktop-v* tags).
The app uses the ccpm CLI for write actions — install it first if you haven't (see Install for the npm / go / curl / source options).
go install github.com/wailsapp/wails/v2/cmd/wails@latest # one-time: the Wails CLI
cd ccpm
make desktop # builds desktop/build/bin/CCPM.app
make desktop-dev # hot-reload dev window| Command | Description |
|---|---|
ccpm add <name> |
Create a new profile (interactive: import wizard + OAuth or API key) |
ccpm run <name> [...] |
Launch Claude Code with a profile. Unknown flags forward to claude. |
ccpm use <name> |
Set the profile for the current shell session (requires ccpm shell-init) |
ccpm list / ls |
List profiles with auth status (--json for machine-readable output) |
ccpm status [name] |
System overview, or auth health for one profile (--json) |
ccpm rename <old> <new> |
Rename a profile (migrates keychain entries and plugin paths) |
ccpm remove <name> / rm |
Delete a profile (--force to skip confirm) |
ccpm clone <src> <new> |
Duplicate a profile (assets + settings + auth; --no-auth for assets only) |
ccpm set-default [name] |
Pin the default profile for direct claude launches; macOS sets it system-wide |
ccpm unset-default |
Clear the default |
ccpm prompt |
Print the active profile name for a shell prompt (PS1 / starship / p10k) |
ccpm statusline |
Render the in-TUI status line (active profile + usage/limits); Claude Code calls it |
ccpm statusline configure |
Choose which segments the status line shows, and on which row |
ccpm sync |
Re-apply global installs into one or all profiles (--dry-run to preview) |
ccpm doctor |
Health check: env, auth, drift, symlinks, cascade (--fix prunes dangling symlinks) |
ccpm consolidate |
Audit (and optionally --fix) asset drift across host, share, and profile scopes |
ccpm export <name> |
Export a profile to a portable .tar.gz (credentials excluded by default) |
ccpm import-bundle <file> |
Restore a profile from an export bundle |
ccpm shell-init |
Print the shell hook (auto-detects zsh / bash / fish / powershell) |
ccpm completion <shell> |
Generate a shell completion script (bash / zsh / fish / powershell) |
ccpm uninstall |
Remove all profiles, keychain entries, vault backups, and ~/.ccpm/ (--force skips the prompt) |
ccpm version |
Print the version; --check-latest checks GitHub for a newer CLI release (opt-in, 24h cache) |
ccpm usage [name] |
Token usage from a profile's transcripts (see Usage) |
ccpm diff <a> <b> |
Compare two profiles (assets, settings keys, env names, MCP servers, plugins) |
Every command also accepts the global --log-level debug|info|warn|error flag (default warn); debug traces cascade adoption and lock activity to stderr.
ccpm run intercepts five flags before forwarding to claude: --ccpm-env KEY=VAL (one-shot env override, repeatable), --no-auto-adopt (skip the host-asset cascade scan for this launch), --no-statusline (skip injecting the default status line for this launch), --help, --version. Use -- to forward --help or --version to claude.
All four share the same command shape. Replace skill with agent, command, or rule.
| Command | Description |
|---|---|
ccpm skill add <path> --global |
Install for every profile |
ccpm skill add <path> --profile <p> |
Install for one profile |
ccpm skill remove <name> --global |
Remove from every profile (alias rm) |
ccpm skill link <name> --profile <p> |
Link a shared asset into a profile |
ccpm skill list |
List installed (alias ls) |
Source may be a directory (skills require a SKILL.md marker) or a single file (agents/commands/rules are usually .md). Pass --live-symlink to keep the source linked so edits show up live, or --copy to snapshot it.
ccpm installs plugins itself, end-to-end, without a Claude Code session. Marketplaces clone into a shared store, plugin files cache once and symlink into each profile, and per-profile activation lives in the same settings fragment ccpm uses for everything else.
| Command | Description |
|---|---|
ccpm plugin marketplace add <org>/<repo> |
Clone a marketplace (HTTPS default; --ssh to use git@) |
ccpm plugin marketplace list / remove <name> |
Inspect or drop a marketplace |
ccpm plugin install <name>@<marketplace> --global |
Install into every profile and enable |
ccpm plugin install <name>@<marketplace> --profile <p> |
Install into one profile only |
ccpm plugin install ... --install-only |
Install without enabling |
ccpm plugin remove <name>@<marketplace> --global / --profile |
Remove the plugin from every profile or just one |
ccpm plugin list [--profile <p>] |
Show installed plugins + per-profile enabled state |
ccpm plugin enable / disable <name>@<marketplace> --profile <p> |
Toggle activation |
ccpm plugin gc |
Drop unreferenced shared-cache entries (also part of sync) |
Plugin clones default to HTTPS so an https://github.com/<org>/<repo>.git URL works without an SSH key. If you prefer SSH, pass --ssh.
| Command | Description |
|---|---|
ccpm hooks add <event> "<cmd>" --profile <p> |
Append a hook to an event |
ccpm hooks add <event> "<cmd>" --matcher <regex> |
Restrict to a tool-name pattern |
ccpm hooks remove <event> --profile <p> [--index] |
Remove the last entry, or one by index |
ccpm hooks list --profile <p> |
Show merged hooks for a profile |
Events: PreToolUse, PostToolUse, UserPromptSubmit, SessionStart, SessionEnd, Notification, Stop, SubagentStop, PreCompact. Hook shell scripts in ~/.claude/hooks/ are managed separately via ccpm import default --only hooks.
| Command | Description |
|---|---|
ccpm mcp add <name> --scope global --command <cmd> |
Stdio MCP for every profile (ccpm global fragment) |
ccpm mcp add <name> --scope profile --profile <p> --command <cmd> |
Stdio MCP for one profile |
ccpm mcp add <name> --scope project --command <cmd> |
Stdio MCP in the current repo's .mcp.json |
ccpm mcp add <name> --transport http --url <url> [--header K=V] |
Remote HTTP MCP |
ccpm mcp add <name> --transport sse --url <url> |
SSE MCP |
ccpm mcp auth <name> --profile <p> |
Complete OAuth in the profile's scope |
ccpm mcp remove <name> --scope <global|profile|project> |
Remove a server |
ccpm mcp import <file.json> --scope ... |
Import from a JSON file ({mcpServers:{...}}) |
ccpm mcp list |
List MCPs with source (--json supported) |
--global and --profile <name> are accepted as aliases for --scope global / --scope profile. For --scope project, ccpm discovers the project root by walking up from CWD looking for .claude/settings.json, .claude/settings.local.json, or .mcp.json, or pass --project-dir <path> explicitly.
ccpm permissions manages permissions.{allow,ask,deny,defaultMode} in the profile fragment (or with --global, in ~/.claude/settings.json). Adding to one bucket removes from the other two so the lists stay disjoint.
| Command | Description |
|---|---|
ccpm permissions allow "Bash(git status:*)" --profile <p> |
Add to permissions.allow |
ccpm permissions ask "Edit(**/*.md)" --profile <p> |
Add to permissions.ask |
ccpm permissions deny "Bash(rm:*)" --profile <p> |
Add to permissions.deny |
ccpm permissions remove "<rule>" --profile <p> |
Strip from all three |
ccpm permissions list --profile <p> |
Show all rules + the default mode |
ccpm permissions mode <default|acceptEdits|plan|auto|dontAsk|bypassPermissions> --profile <p> |
Set permissions.defaultMode |
Trust decides what ccpm's own view (ccpm settings show, the desktop app) takes from a repo's .claude/settings.json and settings.local.json. For an untrusted repo it keeps only inert display, editor and model keys. ccpm never writes project settings or MCP servers into a profile: Claude Code reads them from the repo itself, behind its own trust prompt.
| Command | Description |
|---|---|
ccpm trust add [path] |
Grant trust (defaults to CWD). Alias: grant |
ccpm trust remove [path] |
Revoke trust. Aliases: rm, forget, revoke |
ccpm trust list |
List trusted directories. Alias: ls |
ccpm env persists env vars on a profile; they are added to the process env at every ccpm run. A profile entry replaces the same variable inherited from your shell, and --ccpm-env replaces both.
| Command | Description |
|---|---|
ccpm env set KEY=VALUE [KEY=VALUE...] --profile <p> |
Persist env vars on the profile |
ccpm env unset KEY [KEY...] --profile <p> |
Remove env vars |
ccpm env list --profile <p> |
List persisted env vars |
ccpm run <p> --ccpm-env KEY=VALUE (repeatable) |
One-shot env override at launch time |
CLAUDE_CONFIG_DIR and ANTHROPIC_API_KEY are reserved: ccpm always computes them and they cannot be set via ccpm env. Use --ccpm-env for a one-shot override.
| Command | Description |
|---|---|
ccpm sessions list <profile> |
Sessions scoped to the current working directory, most recently active first (--limit N, --json) |
ccpm sessions list <profile> --all |
Sessions from every project the profile has worked on |
The table shows each session's LAST ACTIVE time. --json gives started (the first timestamp) and last_active. Subagent transcripts are not listed.
ccpm usage [profile] reports token usage read from the profile's Claude Code transcripts: raw input, output, cache-write and cache-read counts, no dollar cost. On a terminal it opens an interactive dashboard (Overview / Days / Models / Projects / Sessions tabs; [ / ] switch profile, w cycles the time window, q quits). With no name it uses the active profile, then the default.
| Command | Description |
|---|---|
ccpm usage [profile] --plain |
Static report: totals, a contribution heatmap, and a per-model breakdown |
ccpm usage --by-model / --by-project / --sessions |
Swap the report body |
ccpm usage --since 30d |
Limit to a window: duration (168h), days (30d), or date (2026-06-01) |
ccpm usage --all |
Aggregate every profile |
ccpm usage --json |
Machine-readable output |
Any of those flags prints a static report instead of the dashboard. Each run reads only the transcript bytes added since the last one. To keep the store warm between runs, ccpm config set usage_tracking true injects a SessionEnd hook (ccpm usage sync) into launched profiles; ccpm usage is accurate without it.
| Command | Description |
|---|---|
ccpm import default --profile <p> |
Interactive: import skills/commands/hooks/agents/rules/settings/MCP/plugins |
ccpm import default --all --only skills |
Non-interactive: import specific targets into every profile |
ccpm import default --profile <p> --no-share |
Copy directly instead of symlinking from the shared store |
ccpm import from-profile --src <a> --profile <b> |
Clone assets from one ccpm profile into another |
--only accepts a comma-separated list of: skills, commands, rules, hooks, agents, settings, mcp, plugins.
ccpm export bundles a profile (its skills, agents, commands, rules and hooks, its settings and MCP fragments, and plugin metadata) into a single .tar.gz you can copy to another machine and restore with ccpm import-bundle. Session history (transcripts, todos, shell snapshots) is left out unless you pass --include-history. ccpm clone duplicates a profile in place.
| Command | Description |
|---|---|
ccpm export <name> [-o file.tar.gz] |
Export a profile to a portable bundle (credentials excluded by default) |
ccpm export <name> --include-history |
Include session history (may contain secrets you pasted into a session) |
ccpm export <name> --include-credentials |
Include credential files (sensitive, trusted same-user moves only) |
ccpm import-bundle <file.tar.gz> [--profile <p>] |
Restore a profile from a bundle (path-traversal-safe; --profile overrides the name) |
ccpm import-bundle <file.tar.gz> --trust-bundle |
Restore hooks, MCP servers, command settings and env without asking (needed without a terminal) |
ccpm clone <src> <new> |
Duplicate a profile, copying assets, settings, and credentials |
ccpm clone <src> <new> --no-auth |
Clone assets/settings only; leave the new profile unauthenticated |
Credentials are NOT included in an export by default: OS-keychain tokens are machine-bound, and .credentials.json / .claude.json hold secrets you usually don't want in a shareable file. Use --include-credentials only for a trusted same-user move (e.g. a Linux machine migration). When a restored or cloned profile has no credentials, authenticate it afterward with ccpm auth refresh <name>.
Treat a bundle from someone else as untrusted code. Before restoring, ccpm import-bundle lists every hook, MCP server, command-running setting and env key in it and asks you to confirm.
OAuth clone caveat: a cloned OAuth profile shares the source account's tokens, so when Claude rotates the refresh token in one, the other goes stale. For a clone you'll use long-term against the same account, prefer
--no-authand runccpm auth refresh <new>to give it its own login. API-key clones have no such caveat.
ccpm completion <shell> prints a completion script for bash, zsh, fish, or powershell, with completion for profile names on run / use / remove / rename / set-default / clone / export.
# zsh: load on every new shell
echo 'source <(ccpm completion zsh)' >> ~/.zshrc
# bash
echo 'source <(ccpm completion bash)' >> ~/.bashrc
# fish
ccpm completion fish > ~/.config/fish/completions/ccpm.fishRun ccpm completion <shell> --help for the per-shell install details.
ccpm prompt prints the active profile name so you can show which Claude Code account a terminal is bound to. It resolves $CCPM_ACTIVE_PROFILE (set by ccpm use), then $CLAUDE_CONFIG_DIR, then — only with --show-default — the configured default. It prints nothing (exit 0) when no profile is active, so it stays quiet in non-ccpm shells.
# bash/zsh PS1
PS1='$(ccpm prompt --format "[ccpm:%s] ")'"$PS1"
# starship custom command
command = "ccpm prompt"Where ccpm prompt feeds your shell prompt, ccpm statusline feeds the Claude Code TUI — two rows pinned to the bottom of the session window. Row 1 is the session; row 2 is the budget:
⬢ work · claude-code-profile-manager/ccpm · ⎇ feat/history-tab · Opus 5 · ctx 34%
effort high · 5h 58% ↺16:15 · 7d 88% ↺Mon 8 Sep 08:25 · $1.23
The 5h / 7d segments are the percentage used of your rolling subscription usage windows, matching Claude's own /usage panel (Pro/Max only — Claude Code supplies them; they appear after the first response and are absent for API-key profiles). effort appears only for models that report a reasoning-effort level. A renewal falling on today's date shows a bare clock; one on any other date gives the day and date. Segments drop out when their data is absent and an empty row is not printed, so an API-key profile outside a git repo prints ⬢ work · myproject · Opus 4.8 and $0.12 — no branch and no usage windows, but the directory still shows.
ccpm run wires this in automatically: when a profile has no statusLine of its own, it injects ccpm statusline as the profile's status line. It never overwrites a status line you set in ~/.claude/settings.json, a profile, or a trusted project. To opt out:
ccpm config set statusline false # persistently, all profiles
ccpm run work --no-statusline # just this launch
ccpm settings statusline "" --profile work # remove one ccpm already injectedccpm statusline configure picks which of the nine segments show, and on which row. It asks for row 1, then row 2 from what is left; anything unpicked is switched off. The result is printed against sample data so you can see it straight away. You are also offered it the first time you run ccpm config set statusline true.
ccpm statusline configure # global default, interactive
ccpm statusline configure --profile work # an override for one profile
ccpm statusline configure --reset # back to the built-in layoutA profile's override wins over the global default; --reset --profile work drops it again. The segment keys are profile, workspace, branch, model, context, effort, five_hour, seven_day, cost, and scripts can skip the prompts by naming all nine across three flags:
ccpm statusline configure --row1 profile,model --row2 five_hour,seven_day \
--off workspace,branch,context,effort,costOrder within a row is yours too: the desktop app has move-up/move-down controls per segment, --row1 a,b,c renders in the order given, and the picker keeps an order you already set rather than re-sorting it. Layouts live in ~/.ccpm/config.json (settings.statusline, or profiles.<name>.statusline). The desktop app's Settings tab has the same controls with a live preview.
Turning a segment on never invents data — enabling the 5h window on an API-key profile still shows nothing. Turning branch off also stops ccpm reading .git/HEAD on every assistant message.
ccpm does not maintain its own global settings layer. The cross-profile baseline is ~/.claude/settings.json (the file native Claude Code reads); ccpm merges it into every profile at launch. Use --profile for per-account overrides, and per-repo .claude/settings.json for project overrides.
| Command | Description |
|---|---|
ccpm settings set <key> <value> --profile <p> |
Set a setting for one profile (dot notation, JSON values) |
ccpm settings get <key> --profile <p> |
Read the effective value |
ccpm settings apply <file.json> --profile <p> |
Deep-merge a JSON fragment |
ccpm settings show --profile <p> |
Dump the fully-merged settings |
ccpm settings statusline "<cmd>" --profile <p> |
Set the native statusLine block. Empty "" removes it. |
ccpm settings outputstyle <style> --profile <p> |
Set outputStyle (allowlist: default, Build, Explanatory, Learning, Direct) |
| Command | Description |
|---|---|
ccpm auth status |
Credential validity across profiles |
ccpm auth refresh <name> |
Re-authenticate a profile |
ccpm auth backup <name> |
Create an encrypted credential backup |
ccpm auth restore <name> |
Restore credentials from backup |
ccpm import default snapshots the files under ~/.claude it touched. Later you can ask whether the host config has drifted:
| Command | Description |
|---|---|
ccpm default fingerprint update |
Record the current ~/.claude state as the baseline |
ccpm default fingerprint check |
Diff ~/.claude against the baseline; suggests import |
ccpm config set check_default_drift true |
Show drift notifications on ccpm run / ccpm use |
| Command | Description |
|---|---|
ccpm config set cascade_auto_adopt false |
Disable the host-asset cascade |
ccpm config set check_default_drift true |
Enable drift notifications on run/use |
ccpm config set statusline false |
Disable default status-line auto-injection |
ccpm statusline configure |
Pick the status line's segments and rows |
ccpm config set usage_tracking true |
Keep the usage store warm via a SessionEnd hook |
ccpm config get default_dir |
Print the default profile's absolute path |
For scripting: 0 success (warnings may print to stderr but never change the
exit code), 1 command failed, 3 partial failure (ccpm import or ccpm sync
did some of its work but at least one step failed), 4 ccpm doctor found health issues.
ccpm run <profile> materializes the profile (merges settings + MCP fragments, links host-cascaded assets), sets CLAUDE_CONFIG_DIR to the profile dir, and execs claude. Each terminal gets a completely isolated Claude Code instance. No daemons, no patches.
- Project:
<repo>/.claude/<asset>/and<repo>/.claude/settings.json. Native Claude Code's loader already gives this priority. - Profile: assets installed via
ccpm <asset> add --profile <name>. - Global: assets installed via
ccpm <asset> add --global(live in~/.ccpm/share/). - Host cascade: anything in
~/.claude/<asset>/(a/plugin installinside a session,npx skills add, manual edits) auto-adopts into every profile at launch. Opt out withccpm config set cascade_auto_adopt falseor--no-auto-adoptonrun/sync. Profile-local always wins over host;ccpm doctorflags shadowed names.
ccpm writes layers 1 to 4 into the profile's settings.json. Layers 5 to 7 are applied by Claude Code itself from the project and system directories; ccpm shows them in ccpm settings show but never writes them into the profile.
- Keys already in
<profile>/settings.jsonthat ccpm didn't write (what Claude Code or you set in a session, such as/model). ~/.claude/settings.json(shared baseline; edit directly to change every profile).~/.ccpm/share/settings/<profile>.json(ccpm per-profile fragment).- Owned-keys sidecar (values explicitly set via
ccpm settings set --profile; protects them from being silently overwritten). ./.claude/settings.jsonat the project root../.claude/settings.local.jsonat the project root (gitignored local overrides).- Enterprise managed-settings (
/Library/Application Support/ClaudeCode/managed-settings.jsonon macOS;/etc/claude-code/managed-settings.jsonon Linux;C:\Program Files\ClaudeCode\managed-settings.jsonon Windows; plusmanaged-settings.d/*.jsondrop-ins merged alphabetically). Highest precedence so org policy always wins.
MCP merge order: host ~/.claude.json#mcpServers → ccpm global fragment → ccpm profile fragment. Claude Code reads project .mcp.json itself.
Removing a setting or MCP server at its source (ccpm mcp remove, ccpm settings unset, deleting it from ~/.claude/settings.json) removes it from the profile on the next launch; values you or Claude Code changed in the profile are kept.
Objects merge key-by-key; arrays and scalars from a higher-precedence source replace the lower one.
~/.ccpm/
├── config.json # profile registry, config keys, status line layouts
├── installs.json # manifest of installed assets (skill/agent/command/rule/hook/mcp/plugin)
│ # entries carry scope: global | profile | host
├── share/
│ ├── skills/ # shared skills (symlinked into profiles)
│ ├── agents/ commands/ rules/ hooks/
│ ├── mcp/ # MCP fragments: global.json + <profile>.json
│ └── settings/ # per-profile settings fragments
├── profiles/
│ ├── personal/ # CLAUDE_CONFIG_DIR for "personal"
│ └── work/
└── vault/ # encrypted credential backups
ccpm is 100% local. It never collects data or phones home, and the CLI makes no network requests except the opt-in ccpm version --check-latest release check. The optional desktop app only contacts GitHub to check for its own updates.
- API keys live in the OS keychain (macOS Keychain, Linux Secret Service, Windows Credential Manager).
- Vault backups use AES-256-GCM with a master key in your OS keychain.
- All data lives in
~/.ccpm/. No telemetry, no analytics, no tracking.
macOS is the verified platform today. Linux and Windows builds compile, install, and run, but the OAuth-isolation paths (
set-default,auth backup/restore, keychain-basedstatus) are experimental until exercised against a real Linux Secret Service or Windows Credential Manager install.
| Feature | macOS ✓ verified | Windows ⚠ experimental | Linux ⚠ experimental |
|---|---|---|---|
| OAuth per-profile | Keychain entry namespaced by profile dir | wincred entry namespaced by profile dir (theoretical) | .credentials.json |
| API key storage | Keychain | Credential Manager | Secret Service |
| Parallel sessions | Yes | Yes | Yes |
| Shared asset dedup | Symlinks | Symlinks (Developer Mode) or copy1 | Symlinks |
| Shell hook | zsh, bash, fish | PowerShell | zsh, bash, fish |
set-default (system-wide GUI) |
LaunchAgent + launchctl setenv |
Keychain / identity only | Keychain / identity only |
Requires Claude Code v2.1.56+ on macOS for OAuth isolation. Earlier versions share a single keychain entry across profiles.
ccpm doctorwarns when your Claude Code is too old.
How an MCP server authenticates determines whether ccpm can isolate it per profile.
- Env-var MCPs (e.g.
GITHUB_TOKEN): isolated. Stored in the per-profile fragment at~/.ccpm/share/mcp/<profile>.json. Configure withccpm mcp add <name> --env KEY=VALUE --profile <p>. - OAuth MCPs (open a browser, cache tokens in
.claude.json#mcpOAuth): isolated, becauseCLAUDE_CONFIG_DIRis per-profile. - Globally-cached MCPs (servers that write to
~/.config/<service>or a non-namespaced OS keychain entry): shared across profiles. ccpm cannot isolate them without cooperation from the server.
- IDE extensions ignore
CLAUDE_CONFIG_DIR: VS Code, Cursor, and Antigravity launch claude directly without going through a shell. Useccpm set-default <profile>to pin one for them. On macOS this is system-wide via a LaunchAgent; on Linux and Windows the keychain and identity sync apply but the system-wide env mechanism is not yet implemented (terminal use is covered byccpm shell-init). - Headless Linux:
go-keyringrequires D-Bus and a secret service (gnome-keyring or kwallet). API-key profiles on headless servers need a running secret service. - Globally-cached MCP servers cannot be isolated per profile (see the MCP auth section).
- macOS keychain logins over about 4 KB (possible once several MCP servers store OAuth tokens in the login) can't be written by
set-default,clone,renameorauth restore. ccpm stops with an error and leaves the keychain unchanged.
Claude Code clones plugin marketplaces over SSH by default. If you authenticate over HTTPS only, force git to rewrite SSH URLs to HTTPS:
git config --global url."https://github.com/".insteadOf "git@github.com:"This applies to every tool that uses git.
ccpm stores credentials in the OS keychain (macOS Keychain, Linux Secret Service, Windows Credential Manager). If access is denied, allow ccpm (and claude) to read the relevant keychain items when prompted. On macOS you can inspect the namespaced entry name via ccpm doctor.
Per-profile OAuth isolation on macOS needs Claude Code v2.1.56+ (older builds share a single keychain entry across profiles). Run ccpm doctor to see your installed version, then update Claude Code.
Run ccpm doctor to detect them, then ccpm doctor --fix to prune dangling shared-asset symlinks. For deeper asset drift across host, share, and profile scopes, use ccpm consolidate --fix.
After ccpm set-default <profile>, already-running IDE windows (VS Code, Cursor, Antigravity) need one restart to pick up the new CLAUDE_CONFIG_DIR. Future launches and logins are automatic.
ccpm set-default, ccpm rename, and ccpm remove don't affect already-running Claude Code sessions. Restart the session (and any IDE window) to pick up the change.
ccpm uninstall removes every profile, deletes API keys and OAuth logins from the OS keychain, wipes vault backups and the vault key, clears the system default set by ccpm set-default, and deletes ~/.ccpm/. Without a terminal it needs --force. It does not remove the ccpm binary itself or the shell hook line you added to ~/.zshrc / ~/.bashrc. The command prints those cleanup steps so you can run them by hand.
# with confirmation prompt
ccpm uninstall
# skip the confirmation
ccpm uninstall --forcescripts/release.sh handles the end-to-end release (bump, verify, tag, GitHub Release, npm publish) with preflight checks (git/go/node/npm/gh on PATH, on main, clean tree, in sync with origin/main, logged in to gh and npm, target tag unused). Run ./scripts/release.sh --help for the full flag list (patch/minor/major/explicit version, --dry-run, --stash, --allow-dirty, --skip-tests, --skip-npm, -y).
Contributions welcome. Open an issue first to discuss what you want to change.
- Fork the repo.
- Create a feature branch (
git checkout -b feature/your-feature). - Make your changes and run the tests (
cd ccpm && go test ./...). - Open a pull request.
MIT
Built by Nitin Gupta.
Footnotes
-
Without Developer Mode or admin, Windows cannot create symlinks; ccpm falls back to copying and writes a marker at
~/.ccpm/.windows-copy-fallback. Turn on Developer Mode for true deduplication. ↩
