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.
curl -fsSL https://raw.githubusercontent.com/rijverse/ctx/main/install.sh | bashThat downloads the source, builds it into ~/.local/share/ctx and links ctx
into ~/.local/bin. CTX_REF installs a branch, tag or commit other than
main, and CTX_INSTALL_DIR and CTX_BIN_DIR move the two locations.
To update, run ctx update. It runs the newest install.sh with the branch and
locations ctx was installed with, and the script installs the newest commit in
place or says ctx is up to date. Running the install command again does the
same. ctx update -n only checks, and --force reinstalls even when nothing
changed (CTX_CHECK=1 and CTX_FORCE=1 for the script). A ctx run from a git
checkout or installed with npm says how to update it instead.
To uninstall, remove both:
rm -rf ~/.local/share/ctx ~/.local/bin/ctxComing soon: ctx is not on npm yet. Once it is published, this will work too:
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.
If claude is running in an account sync is about to change, it is stopped
first, gracefully, with a warning saying which sessions and how to pick them up
again. The session you run ctx from is never stopped.
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.
No Claude accounts on the machine yet? Install Claude Code, then:
ctx init # an empty store
ctx run default # ~/.claude, the account plain `claude` uses: log in, use it, exit
ctx run work # ~/.claude-work: log in with the second accountctx run creates a profile that does not exist yet and links whatever the
store already has before claude starts, so a new account sees the shared
sessions, skills and settings from its first session. When the session ends,
anything the store had no copy of yet moves in and is linked. The first account
to fill the store becomes its seed. If claude starts without asking you to log
in, type /login. After the first ctx run default, plain claude works too.
ctx run only links a profile that is new or already linked. One you detached
on purpose, holding its own copies, is left alone.
To move an existing machine over, use the script in this repo (the install
script leaves a copy in ~/.local/share/ctx/rollout.sh). It shows the
whole plan, asks once, then runs to the end: back up, create the store, fold
every profile in, share the registry, install the hook. -n prints the plan
and stops.
./rollout.sh [--add DIR,DIR] [--seed PROFILE] [-n] [-y]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] |
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 usage [profile...] |
How much of each account's plan limits is left, and when each resets. |
ctx detach [profile...] [--all] |
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. |
ctx update [-n] [--force] |
Update ctx to the newest commit on the branch it was installed from. |
Flags: -n/--dry-run, -y/--yes, -v/--verbose, --json, --only a,b,
--store <path>.
ctx usage profile account plan session week
default me@example.com max 94% 3h 45m 0% 1d 15h
work me@work.example team 18% 5m 83% 4d 11h Fable 100%
spare - - not logged in
The same numbers Claude's /usage shows, for every account at once: the
percent left in the five hour session and the week, any narrower weekly cap
for one model, and the time until each resets. Anthropic reports percentages,
not tokens. It works before ctx init.
Each profile's own login is read and sent only to api.anthropic.com. ctx
never renews or writes a login, because renewing rotates the refresh token and
would log out a claude already running in that account. A login that has
expired shows as expired until claude next runs in that profile.
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. syncanddetachnever stop to make you close sessions.clauderunning in a profile they are about to change is stopped first, going by thesessions/<pid>.jsonfiles Claude keeps: SIGTERM so the transcript is written out, then SIGKILL after five seconds. The plan says which sessions that will be, the run warns as it does it, and--dry-runstops nothing. The sessionctxitself runs from is left alone, with a warning to restart it. An unsynced seed is not a blocker either: it is synced first.- 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.
- Session transcripts and
history.jsonlare the exception: two copies are merged line by line in time order, so nothing either account wrote is hidden. A session two accounts both continued ends up as one transcript with both branches, the same shape Claude writes after a rewind. The originals still go to the backup. - Relative symlinks inside a moved item, like a skill linked from
~/.agents, are rewritten when the move would change what they point at. sync --allfinishes with a pass that links what later profiles brought in, so every profile ends up linked to every shared item.- 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. ctx usagereads logins from.credentials.json. On macOS Claude keeps them in the keychain, so there it says so instead of showing numbers. It also relies on the endpoint behind/usage, which Anthropic does not document.- 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.- When accounts disagree on a plugin's install record, the seed's wins, so a plugin installed only in another account may need installing once more. Its old record is in that account's 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.