One source of truth for every Claude Code account on a machine: shared sessions, skills, settings and trusted projects, separate logins.
Claude Code keeps everything in one directory, ~/.claude by default, and
CLAUDE_CONFIG_DIR moves that directory somewhere else. So running a second
account looks like this:
CLAUDE_CONFIG_DIR=~/.claude-work claudewhich gives you a second, completely separate world: different session history,
different skills, different settings, different everything. That is right for
the login and wrong for the work. ctx keeps the logins apart and puts the work
in one place.
One store holds the only real copy of everything shareable. Each profile directory keeps its identity files and gets a symlink for the rest.
~/.ctx-store/ the only real copy
projects/ skills/ agents/ commands/ rules/ plans/
todos/ file-history/ plugins/
settings.json CLAUDE.md history.jsonl
registry.json the shared half of .claude.json
ctx.json what is shared, the seed, extra profile dirs
.bases/ merge history for .claude.json, per profile
.backups/ anything ctx displaced, per profile and run
~/.claude/ derived
.credentials.json real, yours alone
projects -> ~/.ctx-store/projects
skills -> ~/.ctx-store/skills
settings.json -> ~/.ctx-store/settings.json
~/.claude-me/ same shape, different login
.credentials.json real, someone else's
projects -> ~/.ctx-store/projects
...
Plain claude keeps working with no shell changes, because ~/.claude is just
another profile pointing at the store. Start a session under one account, resume
it under another: it is the same directory on disk.
Node 20 or newer, on Linux or macOS.
npm install -g @rijverse/ctxOr from a checkout:
npm install
npm run build
npm link # optional, puts `ctx` on your PATHand run it in place with node dist/cli.js <command> if you skip the link.
Close every running claude session first. sync refuses a profile while
one is open in it.
ctx init # create the store, with ~/.claude as its seed
ctx sync --all # fold every profile into it, the seed first
ctx status # see the result
ctx run me # launch claude as ~/.claude-me, shared dataNothing is written until you confirm. --dry-run prints the plan and stops,
-y skips the prompt.
To move an existing machine over with a backup first and one trial profile before the rest, use the script in this repo:
./rollout.sh [--add DIR,DIR] [--seed PROFILE] [trial-profile]The store starts from one profile, the seed, and sync always does that one
first. Its settings.json, CLAUDE.md, skills and plugins become everyone's,
and when another profile has a file of the same name, the store's copy wins and
the other one goes to that profile's backup. The seed is ~/.claude unless you
say otherwise with ctx init --from <profile>, and is recorded in ctx.json.
With no ~/.claude and more than one profile, init asks rather than guesses.
A sync that would let another profile get in ahead of the seed is refused
until the seed is synced.
CLAUDE_CONFIG_DIR can point anywhere. ~/.claude and ~/.claude-* are found
on their own. A config dir under any other name, ~/.my-claude say, is spotted
when CLAUDE_CONFIG_DIR points at it in your shell or a shell startup file
(.zshrc, .bashrc, an alias, fish's config.fish), or when a directory in
~ holds a .claude.json or .credentials.json. Those are only suggested:
init asks about each one, status lists them, and ctx run my-claude tells
you to add it instead of making a new profile. A backup copy of a profile looks
exactly like a profile, so nothing is taken in without a yes. Adding by path:
ctx init --add ~/work/.claude,~/accounts/alpha # before there is a store
ctx add ~/clients/acme/.claude # after
ctx add ~/clients/acme/.claude --as acme-client # pick the name yourself~/.claude-work is called work, a .claude directory is named after its
parent (~/work/.claude is work), and any other directory goes by its own
name. Two profiles can never share a name, because backups and merge history
are kept by it. When two would, ctx stops and asks for --as.
Shared, moved into the store once and symlinked back:
projects |
session transcripts, the reason this exists |
todos, file-history, session-env, tasks, paste-cache |
the state that makes a transcript resumable |
history.jsonl |
prompt history |
skills, agents, commands, output-styles, rules, plans, memory |
things you wrote |
plugins |
marketplaces and installed plugins |
settings.json, CLAUDE.md, downloads |
Never shared, and ctx refuses to move them even if you edit the config to say
otherwise:
.credentials.json, .claude.json, policy-limits.json (and its
.stamp.json), remote-settings.json, stats-cache.json,
settings.local.json, mcp-needs-auth-cache.json
Left alone entirely, neither shared nor guarded: cache, sessions,
shell-snapshots, ide, telemetry, backups, chrome, daemon, jobs,
feedback.
Anything Claude adds that ctx has never heard of is left alone. A new release
cannot quietly get something shared.
.claude.json is the awkward one. It holds who you are logged in as and what
you have been working on, in the same file, so it can be neither symlinked nor
ignored. ctx owns an allowlist of top-level keys and leaves the rest of the
file untouched:
shared, kept in registry.json |
stays with the profile |
|---|---|
projects (trust decisions, per-project MCP servers, allowed tools) |
oauthAccount, userID, machineID |
mcpServers |
subscription and entitlement caches |
tipsHistory, skillUsage, pluginUsage, githubRepoPaths |
startup counters, migration flags |
hasCompletedOnboarding, promptQueueUseCount |
everything else, including keys added by future releases |
ctx run <profile> folds the profile into the store and writes the shared keys
back in before launching, then folds what the session changed back out
afterwards. Folding is a three-way merge against what that profile and the store
last agreed on, kept in <store>/.bases/:
- A change on one side wins over a side that did not touch it, deletions included. Remove an MCP server in one account and it is gone from all of them.
- Objects merge entry by entry, so two accounts working on different repos never erase each other.
- When both sides changed the same value, lists such as
allowedToolsmerge as sets, a flag either side set (like a trust decision) stays set, an edit beats a deletion, and otherwise the profile wins as the latest writer. - A profile whose project registry suddenly went empty looks reset, not
curated. It is restored from the store instead of being merged into it, so
one wiped
.claude.jsoncannot wipe every account. - A
.claude.jsonthat does not parse is never read as empty.ctxleaves it untouched and says so, andctx runstill launchesclaudeso it can recover the file itself.
Writes to .claude.json take the same <file>.lock Claude takes when it saves
the file, so a pull never lands in the middle of a running session's save.
The practical effect: trust a repo once, and every account trusts it. Add an MCP server once, and every account has it. Log in once per account, and that login stays put.
ctx registry show # what the store holds
ctx registry diff me # store against one profile
ctx registry pull me # store -> profile
ctx registry push me # profile -> storeIf you start claude some other way, ctx hooks install adds a SessionEnd
hook to the shared settings.json so the session still folds its changes back
into the store on exit. A non-default --store is written into the hook, and
installing again after moving ctx updates the path in place.
Only the push half is hookable. SessionStart fires after Claude has already
read .claude.json, so pulling there would be overwritten by Claude's own next
save. Pulling stays the job of ctx run.
| Command | What it does |
|---|---|
ctx init [--from <profile>] [--add <dir>,...] [--as <name>] |
Create the store and pick its seed. |
ctx add <dir>... [--as <name>] |
Manage a profile outside ~/.claude-*, or rename one. |
ctx sync [profile...] [--all] [--force] |
Move a profile's data into the store and symlink it back. |
ctx status (aliases ls, st) |
Per-profile, per-item state in one grid, plus Claude dirs not yet managed. |
ctx run <profile> [-- <args>] |
Launch claude with the shared registry in place. |
ctx detach [profile...] [--all] [--force] |
Give a profile back its own independent copies. |
ctx doctor [profile...] (alias repair) |
Find and repair broken or misdirected links, and flag real copies where a link should be. |
ctx registry <show|pull|push|diff> |
Drive the .claude.json split by hand. |
ctx hooks <status|install|uninstall> |
Wire sessions you did not start with ctx run into the store. |
ctx which [profile] |
Print the CLAUDE_CONFIG_DIR export for a profile. |
Flags: -n/--dry-run, -y/--yes, -v/--verbose, --json, --only a,b,
--store <path>, --force.
The tool is built so an interrupted run is always safe to re-run.
- Real data is moved, never deleted. It goes into the store, or into
<store>/.backups/<profile>/<timestamp>/. The only thingsctxdeletes outright are a symlink it is about to replace and directories with no files left in them. A file that turns up mid-merge goes to the backup. syncanddetachrefuse a profile whileclaudeis running in it, going by thesessions/<pid>.jsonfiles Claude keeps. Close those sessions, or pass--forceif you know better. Session files left by a crashedclaudeare ignored.- Links are swapped with a rename, so a path is never briefly missing for a running session to recreate as a real directory.
- Everything that changes the store runs under one lock, so two
ctxprocesses, or two sessions ending at once, take turns. - Every step runs in the order merge, back up, then link, so the store is a superset before a profile's copy goes away.
- Directories are merged without clobbering. When the same path exists on both sides the store copy wins and yours is kept in the backup, reported by count at the end.
- Identity files are guarded in code, not just in config.
- Every destructive command prints its plan and asks once.
ctx detachreads from the live store, not from backups, so it works even if you deleted them.- The store holds everything sensitive Claude keeps apart from logins:
transcripts, prompt history, and MCP server settings that may carry API keys.
Treat it like
~/.claude. Its registry files are written0600.
ctx hooks uninstall # first, or every profile keeps a copy of the hook
ctx detach --all # every profile gets real copies backAfter that the profiles no longer use the store. Keep ~/.ctx-store until you
are happy, since .backups/ holds anything ctx displaced along the way, then
archive or remove it yourself.
- Linux. Symlink behavior on macOS should work the same way but is untested.
Without
/proc, a session or lock holder is recognised by pid alone, not pid and start time. Windows is out of scope. - Two sessions writing the shared
projects/at once is fine, since transcripts are separate files with unique ids. history.jsonlis a single append-only file, so two sessions writing it at once can interleave. Drop it fromsharedinctx.jsonif that bothers you. It is also not merged when profiles are first folded in: the seed's history is kept, and every other profile's goes to its backup.- Removing every MCP server, or every project, from one account reads as a
reset and does not spread. Remove them one at a time, or edit
<store>/registry.json, if that is really what you want. - A session started without
ctx runonly gets the shared keys the next time that profile is pulled. The hook covers the push half only. plugins/caches marketplace checkouts. Sharing it is usually what you want and is in the defaults, but it is the item most likely to surprise you.
~/.ctx-store/ctx.json, written by ctx init and safe to edit:
{
"version": 1,
"shared": ["projects", "todos", "skills", "settings.json"],
"registryKeys": ["projects", "mcpServers"],
"backup": true,
"profiles": ["/home/me/work/.claude", { "dir": "/home/me/acme", "name": "acme-client" }],
"seed": "default"
}Unknown or guarded names in shared are dropped on load rather than rejected,
so a bad edit shares less instead of breaking the tool. ~/.claude and every
~/.claude-* directory that is empty or holds something only Claude writes
there (.claude.json, projects, settings.json and so on) are discovered.
Other tools that live in ~/.claude-*, and old ctx stores, are skipped.
profiles adds directories on top of those, by path or with a name, and is
what ctx add writes. A store that sits inside a profile, or the other way
round, is refused.
| variable | effect |
|---|---|
CTX_STORE |
store location, same as --store |
CTX_HOME |
treat another directory as home, which is how the tests stay off yours |
CTX_DEBUG |
print a stack trace with unexpected errors |
NO_COLOR |
plain output |
CLAUDE_CONFIG_DIR |
read by the hook to know which profile a session belonged to |
npm testTests run against throwaway directories under $TMPDIR via CTX_HOME, with
HOME also pointed at a throwaway directory and a fake claude first on
PATH, so they never read or write your real ~/.claude* or start the real
claude.