Skip to content
rijversePublic

About

No description, website, or topics provided.

Resources

Stars

1 star

Watchers

0 watching

Forks

Repository files navigation

ctx

ci license: MIT

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 claude

which 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.

How it works

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.

Install

Node 20 or newer, on Linux or macOS.

curl -fsSL https://raw.githubusercontent.com/rijverse/ctx/main/install.sh | bash

That 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/ctx

Coming soon: ctx is not on npm yet. Once it is published, this will work too:

npm install -g @rijverse/ctx

Or from a checkout:

npm install
npm run build
npm link          # optional, puts `ctx` on your PATH

and run it in place with node dist/cli.js <command> if you skip the link.

Quick start

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 data

Nothing is written until you confirm. --dry-run prints the plan and stops, -y skips the prompt.

Starting from nothing

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 account

ctx 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.

Moving an existing machine over

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 seed

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.

Profiles with other names

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.

What is shared, and what is never shared

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.

The .claude.json split

.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 allowedTools merge 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.json cannot wipe every account.
  • A .claude.json that does not parse is never read as empty. ctx leaves it untouched and says so, and ctx run still launches claude so 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 -> store

If 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.

Commands

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>.

Which account has room left

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.

Safety

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 things ctx deletes 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.
  • sync and detach never stop to make you close sessions. claude running in a profile they are about to change is stopped first, going by the sessions/<pid>.json files 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-run stops nothing. The session ctx itself 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 ctx processes, 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.jsonl are 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 --all finishes 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 detach reads 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 written 0600.

Undo

ctx hooks uninstall     # first, or every profile keeps a copy of the hook
ctx detach --all        # every profile gets real copies back

After 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.

Caveats

  • 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 usage reads 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.jsonl is a single append-only file, so two sessions writing it at once can interleave. Drop it from shared in ctx.json if 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 run only 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.

Configuration

~/.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.

Environment

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

Testing

npm test

Tests 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.

About

No description, website, or topics provided.

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages