How it works

Built so a bug can't cost you a transcript. Here is how.

Baton touches the folders that hold your Claude Code history, so it is built like a tool that has to be trusted with them. This page explains the design, the safety engine, what Baton will never do, and how it is tested, with links to the code for every claim.

  • 39sandbox tests, 368 checks, 2.2 s
  • 0network requests made by Baton itself
  • 0tokens or Keychain items read
  • 10primitive file steps, each with an inverse

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 →

BATON.APP Main windowsandboxed, CSP self Tray popoversmaller preload Main processplan · preview · launch gate · usage Safety engineguard · snapshot · journal · rollback Swift helpericons, focus, quit Sync enginewatches index files IPC IPC (subset) operations sync ops writes index files HOME FOLDER ~/.claude/projectsthe one transcript storeadd-only ~/.claude-workprojects/ holds onelink per shared project symlink ~/.batonstate · journal · verified snapshots · hidden sidebar backups · sync state ~/LIBRARY/APPLICATION SUPPORT Claude/default app's datalocal_*.json index Claude-Work/Work app's datalocal_*.json index syncednewest wins Claude.appin /Applications,never modified Claude Work.appAPFS clone,tinted icon cp -Rc --user-data-dir
The two windows can only ask the main process for things over a fixed list of IPC channels. Writes go through the safety engine, which allows add-only changes in the transcript store, links in extra accounts, and only Code-tab index files inside desktop data folders. The Swift helper (called with execFile) tints icons and focuses or quits apps.

03

How sharing a project works

Account 1's ~/.claude/projects stays the only place transcripts live. Sharing a project with another account adds exactly one symlink inside that account's own projects/ folder. When claude runs as that account, it writes through the link straight into the shared store, so Terminal sessions need no syncing at all.

For the desktop apps, transcripts already land in the store. What differs is each app's sidebar index, so Baton mirrors only the index files whose cwd is inside the shared project. A file is read only when it is stable (same size and mtime before and after the read, and valid JSON). The newest version wins; the older one is kept in a snapshot. Deletions never propagate, and apps that are open are never written to.

Scroll sideways to see the whole diagram →

TERMINAL claude, as WorkCLAUDE_CONFIG_DIR=~/.claude-work ~/.claude-work/projects/-Users-me-acme-appa symlink, nothing else writes resolves ~/.claude/projects/-Users-me-acme-app/ s1.jsonls2.jsonl s3.jsonlnew, from Work add-only: nothing is overwritten DESKTOP APPS Claude (default)sidebar indexlocal_s3.json Claude Worksidebar indexlocal_s3.json syncednewest wins every app's transcripts Turning Work off 1. The link is removed. Work's transcripts stay in the store. 2. Work's sidebar entries move to ~/.baton/hidden/ with a manifest. Turning Work on again restores them.
Sharing acme-app with a second account called Work. The Terminal side is one link. The desktop side is a mirror of small index files between apps. Account 1's apps are never hidden from.

Code: electron/sharing.ts (shareTerminalSteps, planSync, readStable, planHide, planRestoreHidden).

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 →

1Plansteps from disknothing written 2Previewevery file opguard checked 3Confirmyou approvethis exact plan 4App checkClaude open onthese? stop. 5Snapshottar.gz + sha256of touched paths 6Verify backupextract andre-hash all bad hash Stopnothing waschanged 7Journalpre-state savedbefore each step 8Applyone step at atime, re-guarded 9Check resultlinks, hashesand moves 10Donesnapshot kept,in History Undo latera new operationfrom the backup throws mismatch Crash / quitrolled back onnext start Roll back automaticallyinverse steps from the journal, newest firsteach one checks the disk first, so it is safe twice Exactly as beforejournal says rolled-backthe error and every undo step are in History
The pipeline in electron/safety.ts, runOperation. Red paths are the failure cases; every one ends with your folders as they were.
  1. PlanA planner reads the disk and builds steps. It writes nothing, except staging copies under ~/.baton/work/.
  2. 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.
  3. ConfirmThe engine refuses to run anything that was not confirmed, and only the previewed plan runs.
  4. Running-app checkA claude process using a touched account folder, or a desktop app whose --user-data-dir is 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.
  5. 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.
  6. 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.
  7. JournalStatus applying, the steps, and a slot for each step's previous state, written atomically.
  8. 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.
  9. Check the resultLinks point where they should, written files hash to their source, moved paths are where they belong.
  10. 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.
  11. 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 with SIGKILL mid-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.

WhereWhat Baton may doWhy
/Applications/Claude.appNothing. Clones are copied from it.It is Anthropic's signed app.
~/.claude/projectsCreate 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 ~/.claudeNothing (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 foldersWrite 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 copiesBuild or remove *.app only inside Baton's own apps folder.Never another tool's copies (tested).
LinksRemove 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

Chose

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.

Said no to

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

Chose

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.

Said no to

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

Chose

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.

Said no to

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

Chose

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.

Said no to

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

Chose

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.

Said no to

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

Chose

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.

Said no to

Deleting the entries. Transcripts would survive, but titles and sidebar state would be gone for good.

sharing.ts · planHide, planRestoreHidden

No server at all

Chose

Everything happens on your Mac, in files you can inspect. Updates are a new download from GitHub Releases.

Said no to

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:

WhoWhatWhen
Baton's own processNone. No fetch, sockets, analytics, crash reports or update checks.Never
Baton's windowsNone. Blocked by the CSP.Never
Official claude CLI, run by Batonclaude -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 Terminalclaude 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 copiesWhatever Claude's desktop app normally does. They are unmodified.When you open one
Your browserLinks 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 claude for 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.

npm test
✓ 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)
AreaWhat the tests prove
EngineRestores 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.
CrashA 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.
SharingShare and unshare change only links; merging private history adds files only; migration from whole-folder links works and undoes.
SyncTwo-way, newest wins with the loser kept, safe with atomic-rename writes, never writes to a running app.
Hide and restoreOnly the account's own apps are hidden from, never account 1's; idempotent; a newer file is kept on restore.
Desktop appsCreate, recolor, update and remove through the engine; adopting an existing data folder leaves it untouched; tidy-up never touches another tool's folders.
Fair useEight 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.
TrayPopover 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 /usage are 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 statusLine key in that account's settings.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