01
The problem, in file paths
Claude Code keeps everything for a sign-in in one folder: ~/.claude. A second account means setting CLAUDE_CONFIG_DIR to another folder, and that gives you a second, separate history. Account 1 writes to ~/.claude/projects/<project>/, account 2 to ~/.claude-work/projects/<project>/. When account 1 runs out of usage halfway through a task, claude --continue under account 2 finds nothing.
The Claude desktop app has its own version of this. It is an Electron app with one sign-in per data folder. Its Code tab writes every transcript into ~/.claude/projects regardless of account, but what its sidebar shows comes from a separate, per-app index: one small local_<id>.json file per session under claude-code-sessions/<account>/<org>/. The app rewrites those files by atomic rename, and their format is not documented.
So Baton has to do two things: let your own accounts continue the same project history, and keep each desktop app's sidebar in step, without copying a single credential and without ever putting the transcripts at risk.
02
System overview
Baton is an Electron app: a sandboxed React window, a sandboxed menu-bar popover, a main process in TypeScript (about 5,800 lines), and a 100-line Swift helper. Every change to your Claude folders passes through one module, the safety engine. There is no server.
Scroll sideways to see the whole diagram →
execFile) tints icons and focuses or quits apps.04
The safety engine
Every change Baton makes to your Claude folders is an operation: a list of primitive steps (make a folder, create a link, remove a link, write a file atomically, move something aside, hide a sidebar entry, quarantine, build or remove an app copy, move to the Trash) plus the paths it touches. The engine runs every operation through the same pipeline.
Scroll sideways to see the whole diagram →
runOperation. Red paths are the failure cases; every one ends with your folders as they were.- PlanA planner reads the disk and builds steps. It writes nothing, except staging copies under
~/.baton/work/. - Preview (dry run)Every step is described in plain words, checked against the guard list, and checked for running Claude processes. The plan waits under a random token for 30 minutes.
- ConfirmThe engine refuses to run anything that was not confirmed, and only the previewed plan runs.
- Running-app checkA
claudeprocess using a touched account folder, or a desktop app whose--user-data-diris touched, blocks the operation. Baton can ask the desktop app to quit, wait up to 20 s, apply, and reopen it. If it doesn't quit, nothing changes. - SnapshotEvery touched path is walked without following links into a manifest (type, mode, mtime, link target, sha256 per file) and archived with
/usr/bin/tar. Over 512 MB is refused: a correct plan never touches transcripts, so it is always small. - Verify the snapshotThe archive is checksummed, extracted into a scratch folder and compared entry by entry with the manifest. Any difference stops the operation before the first change.
- JournalStatus
applying, the steps, and a slot for each step's previous state, written atomically. - ApplyBefore each step the guard list runs again, this time resolving every parent folder through symlinks, so a write that would reach a protected folder by going through a link is refused. The step's previous state (did it exist, old link target, a copy of the old file) is saved to the journal before anything changes.
- Check the resultLinks point where they should, written files hash to their source, moved paths are where they belong.
- DoneThe snapshot stays (retention keeps at least the newest 5 per kind, and never prunes snapshots that hold a sync conflict's losing version or quarantined items). Undo builds a new operation from the snapshot, with its own preview, so an undo can be undone too.
- On any failureInverse steps run from the journal, newest first. Each checks the current state before acting, so a half-applied step or a second rollback is safe. A crash leaves the journal in
applying; the next start rolls it back and tells you. A test kills the process withSIGKILLmid-operation to prove it.
The guard list
Checked when planning and again immediately before each step (guardStep). Paths are compared case-insensitively, because APFS is.
| Where | What Baton may do | Why |
|---|---|---|
/Applications/Claude.app | Nothing. Clones are copied from it. | It is Anthropic's signed app. |
~/.claude/projects | Create new files and folders only. Never overwrite, move or delete. | It is the only copy of your history. Even a rollback keeps a new file there. |
The rest of ~/.claude | Nothing (one opt-in exception, see What could break). | Account 1 is your setup from before Baton. Uninstalling Baton leaves Claude as it was. |
| Desktop app data folders | Write or hide local_*.json index files; create a brand-new app's folder; send a whole extra app's folder to the Trash. | The rest holds the app's sign-in, settings and storage. Baton only needs the sidebar index. |
| App copies | Build or remove *.app only inside Baton's own apps folder. | Never another tool's copies (tested). |
| Links | Remove a path only if it is a symlink. | Removing a link never touches what it points at. |
05
Decisions we made, and what we said no to
Share one project at a time
One symlink per shared project inside the account's real projects/ folder. If the account already had its own history there, Baton adds its files to the store only where no file of that name exists, and keeps the old folder renamed beside the link.
Linking the whole projects/ folder. Early versions did, and it shares every project, including ones that belong on a personal account. Baton detects that setup and offers a previewed, undoable migration away from it.
sharing.ts · shareTerminalSteps, planMigration
Run Claude's own app, unmodified
A copy-on-write APFS clone of Claude.app per account (cp -Rc, almost no disk), launched with its own --user-data-dir. The color is a Finder icon outside the code seal, so the signature still verifies and the Keychain still trusts it. Clones are rebuilt when Claude updates.
Patching the Claude bundle or injecting code. That breaks Anthropic's signature and Keychain trust, and modifying Claude is off the table on principle.
desktop.ts · buildClone, openProfile
Copy whole index files; newest wins, the other is kept
Treat each sidebar file as opaque bytes. On a conflict the newest version wins, ties are broken by hash so the result is deterministic, and the losing version is kept in a pinned snapshot with a notification.
Merging fields (the format is undocumented and changes between releases), per-file symlinks (the app's atomic renames replace them on the first save), and folder symlinks (they share every project's chats at once).
sharing.ts · planSync, syncOperation
Never read a token
Allow-lists. From .claude.json, only the email and four plan/organization type fields. From a desktop app's config, only the account id and whether a sign-in exists. From a running claude's environment, three keys; the rest of the string is dropped at once. Every sign-in happens in Claude's own flow.
Copying sign-ins between accounts, reading the Keychain, or calling Anthropic's APIs with your token.
core.ts · readEmail, pickEnv · fairuse.ts · KIND_HINT_KEYS
Usage from the official CLI, on demand
claude -p /usage, which reads plan limits without a model call. It runs only when Baton opens or you press Refresh, at most once per 15 minutes per account, and the gate survives restarts. An optional status-line hook captures limits Claude Code already reports, for free.
Polling on a timer, calling the API directly, or scraping claude.ai. Usage is shown, never acted on: no alerts, and no "switch accounts" suggestions.
usage.ts · probeUsage · fairuse.ts · ProbeGate
Unsharing hides, it doesn't delete
Turning an account off removes its link and moves the project's sidebar entries from that account's apps into ~/.baton/hidden/, with a manifest of source paths and hashes. Turning it back on restores them, keeping any newer file already in place.
Deleting the entries. Transcripts would survive, but titles and sidebar state would be gone for good.
sharing.ts · planHide, planRestoreHidden
No server at all
Everything happens on your Mac, in files you can inspect. Updates are a new download from GitHub Releases.
Accounts, telemetry, crash reporting, and an auto-updater that phones home.
06
What Baton will never do
- Delete or overwrite a transcriptThe store is add-only, enforced by the guard list before every step.
- Delete your dataRemoving means unlinking a symlink, quarantining into a snapshot, hiding into a backup, or the macOS Trash.
- Read your tokens or the KeychainOnly allow-listed fields are taken from config files.
- Copy a sign-in between accountsEvery account and app signs in through Claude's own flow.
- Modify Claude.app or Claude CodeApp copies are byte-identical clones; the CLI is run as installed.
- Send anything over the networkBaton's process has no network code; its windows are locked to local content.
- Change anything without showing you firstEvery operation has a dry-run preview. The one exception is background sync for projects you chose to share, which still snapshots and journals every pass.
- Help you dodge usage limits or a banNo limit alerts or account rotation; a suspended account's projects aren't offered to other accounts.
What Baton does delete is its own material: rebuildable Claude.app copies, empty folders it created when rolling back, its staging folders, and its own old snapshots under retention. Never a file it did not create.
07
Network and privacy
Both windows run with context isolation, the Chromium sandbox and no Node access, under a content security policy of default-src 'self'. They reach the main process only through small preload bridges. The menu-bar popover's bridge is smaller still: it cannot confirm accounts, change sharing or run operations, and its handlers check that the request really came from the popover.
Here is every outbound connection that happens because of Baton:
| Who | What | When |
|---|---|---|
| Baton's own process | None. No fetch, sockets, analytics, crash reports or update checks. | Never |
| Baton's windows | None. Blocked by the CSP. | Never |
Official claude CLI, run by Baton | claude -p /usage (plan limits, no model call) and claude auth status, with a cleaned environment. | When Baton opens (gated) and when you press Refresh |
Official claude CLI, in Terminal | claude auth login and the sessions you start. Baton writes a launch script; Terminal runs it. | When you click Sign in or Continue |
| Claude desktop app copies | Whatever Claude's desktop app normally does. They are unmodified. | When you open one |
| Your browser | Links you click: Anthropic's policies and support, GitHub Sponsors. | On click |
Baton also scrubs inherited CLAUDE_* and ANTHROPIC_* variables from its own environment at startup. Without that, starting Baton from inside a Claude session would make every claude it launches sign in as that session's account. Diagnostics exports redact tokens, emails and your home path.
08
Fair-use guardrails, as code
Baton is for one person using their own accounts. The rules live in electron/fairuse.ts, and one launch gate covers the main window, the native tray menu and the popover.
- Account kind (Personal or Work) is detected from non-secret plan and organization fields, by whole-word match. When nothing is clear, Baton asks.
- Ownership: "This account is mine, and only I use it." is required and timestamped before Baton opens
claudefor an account. - A second Personal plan also needs "I'm not using this to get around usage limits."
- Suspended accounts are detected only from clear wording that the account was suspended, banned or disabled; phrasings like "will be suspended if" or "not suspended" never match (25 checks). Their projects are refused for other accounts, in launches and in sharing.
- Work accounts: sharing into one, or out of one into a Personal account, adds a notice to the preview.
These are guardrails, not enforcement. Baton can't verify who owns an account, detection depends on CLI wording that can change, and a determined person can edit Baton's state file. The aim is that Baton never makes the wrong thing easy and never suggests it.
09
How it's tested
Every test runs in a fake home folder built fresh in a temp directory: an account 1 store with transcripts, an old-style account with a whole-folder link, a new-style account, three desktop data folders, a tiny stand-in Claude.app and a fake Trash. All roots come from one overridable module, and the harness aborts if they ever resolve to the real home folder.
✓ snapshot → change → restore is byte-for-byte; corrupt snapshots fail verification (8 checks) ✓ share merges an account’s private history (adds only) and undo brings it back (8 checks) ✓ migration: whole-history link + old desktop links → per-project sharing; undo restores (20 checks) ✓ crash mid-operation (process killed between steps) → rolled back on next start (11 checks) ✓ a failing step rolls back automatically (8 checks) ✓ desktop sync: two-way, conflict = newest wins and the loser is kept (9 checks) ✓ desktop sync is safe with atomic-rename writes (5 checks) ✓ unshare hides the project’s chats from the account’s desktop apps (never account 1’s); idempotent; undo (17 checks) ✓ read-only mode (used for real-machine checks) never writes (4 checks) … 30 more 39/39 tests passed, 368 checks, 2.2s — sandbox only (real HOME never touched)
| Area | What the tests prove |
|---|---|
| Engine | Restores are byte-for-byte; a corrupted snapshot fails verification; a failing step rolls back; retention never prunes the newest 5, pinned or quarantined snapshots; read-only mode writes nothing. |
| Crash | A child process is killed with SIGKILL after step 3 of a migration. The journal is left applying, Doctor flags it, and recovery restores every watched folder exactly. |
| Sharing | Share and unshare change only links; merging private history adds files only; migration from whole-folder links works and undoes. |
| Sync | Two-way, newest wins with the loser kept, safe with atomic-rename writes, never writes to a running app. |
| Hide and restore | Only the account's own apps are hidden from, never account 1's; idempotent; a newer file is kept on restore. |
| Desktop apps | Create, recolor, update and remove through the engine; adopting an existing data folder leaves it untouched; tidy-up never touches another tool's folders. |
| Fair use | Eight tests: kind detection from non-secret fields, attestation, the second-plan tick, no limit-dodging paths in the UI, Work notices, suspension detection, the usage gate, single-person scope. |
| Tray | Popover placement across displays; the popover runs sandboxed and goes through the same gates as the main window. |
On a real Mac, npm run check:real runs Doctor and the migration preview against your actual folders in read-only mode, where every write path in the engine throws, and compares a listing of Baton's state folder before and after to prove nothing changed. For screenshots, npm run shots runs the real app against a generated fake home with claude and osascript blocked and every preview cancelled.
Not covered by automated tests: the Swift helper, most of the IPC wiring, the React UI, and real interaction with Claude.app and the claude CLI. Those are checked by hand against real installs, which is why every fact about Claude's file layout in the code carries the Claude version it was verified on.
10
What could break
- Undocumented formatsThe sidebar index files, the project-folder naming, the desktop config's account id and the text of
/usageare Anthropic's implementation details. A Claude update can change them. Baton copies index files as opaque bytes and Doctor reports what it can't read, but a change can stop sync or titles until Baton is updated. - Where desktop transcripts goDesktop sharing relies on the Code tab writing every transcript to
~/.claude/projects. If Claude changes that, this part needs a new design. - First sign-in in a new app copyClones share Claude's bundle id, so the login callback goes to whichever Claude app is running. Baton asks the others to quit first.
- Apple silicon, macOS, Terminal.appRelease builds are arm64 only; an Intel build from source is untested. Launches use Terminal.app.
- Some steps are undone by handTrash moves come back with Finder › Put Back; a removed app copy is rebuilt the next time you open it.
- Optional usage capture edits one settingIf you turn it on for an account, Baton changes the
statusLinekey in that account'ssettings.json, after a confirm that shows the before and after, with a backup of the whole file and a restore when you turn it off. For account 1 that file is~/.claude/settings.json: the one place Baton can write inside~/.claude, and only after a warning. - Removing an accountIts folder goes to the Trash, but its Keychain sign-in item stays, because Baton never touches the Keychain.
Releases are signed with a Developer ID, built with the hardened runtime, notarized by Apple and verified with codesign, spctl and stapler before publishing, with a SHA256SUMS.txt next to the DMG. The steps are in RELEASING.md.
11
Read the code
electron/safety.tsThe safety engine: guard list, snapshots, journal, apply, verify, rollback, undo.electron/sharing.tsPer-project links, the index sync engine, hide and restore, migration, Doctor.electron/desktop.tsAPFS clones of Claude.app and launching them with their own data folder.electron/apps.tsPairing desktop apps with accounts by email; merge and tidy up.electron/fairuse.tsAccount kind, attestation, suspension detection, the usage gate and the launch gate.electron/usage.tsUsage limits from the official CLI, and the optional status-line capture.electron/core.tsAccounts, session titles, process detection, environment scrubbing, launch scripts.electron/roots.tsEvery filesystem root in one place, and read-only mode.helper/baton-desktop-helper.swiftThe Swift helper: tint an icon, set it, focus or quit an app.scripts/test.tsThe sandbox test suite, including the SIGKILL crash test.ARCHITECTURE.mdThis page in more depth, with function names for every claim.FAIR-USE.mdWhat Baton is for, and what it won't help with.