carryon
Carries an AI coding agent's working life between machines: the Setup that makes an agent yours — settings, skills, subagents, instructions, plugin lists — and the History of what you did with it.
carryon reports what it sees and encrypts what it carries. Both halves are
sealed under a master key before they leave the machine, so a credential found
in either — an API key in settings.json's env block, a token echoed into a
transcript — is reported and carried, never redacted or blocked. The scan is a
list of shapes it recognises rather than a proof (an opaque token matches
none), which is why the promise rests on encryption and not on the scan.
What a capture may read is still bounded: carryon refuses a Setup whose
capture set reads its own state or anything outside $HOME, or when it cannot
tell whether it did. And a Setup carries what makes an agent yours, not what
built it: inside a tree an Adapter declares, development artifacts such as a
skill's test suite are left out and named, while a path you add to carry
yourself is carried whole.
Supports Claude Code, Codex CLI, Cursor, and the ~/.agents skills
convention; a History moves for Claude Code and Codex, the two whose session
layout the engine knows.
The vocabulary used throughout — Setup, History, Archive, Destination, Session — is defined in CONTEXT.md, and the reasoning behind each decision is in docs/adr/.
What you need
- macOS or Linux. The suite runs on both in CI, but the agents' own layouts have been checked on macOS only (see Platform support). Nothing has been run on Windows.
- Python 3.9 or newer. carryon has no Python dependencies.
openssl, which macOS and Linux distributions already have. Encryption shells out to it, so the machine you are moving to needs nothing installed beyond carryon itself.- A Destination: somewhere both machines can reach. Any one of these:
- a synced folder (Dropbox, Google Drive, iCloud Drive, Syncthing). No account beyond the one the folder already uses.
- a private git repository you can already push to.
- an rclone remote (S3, Cloudflare R2, B2, SFTP, ...).
initcan set one up for R2, S3, Google Cloud Storage or SFTP if you havercloneinstalled.
- A password manager, to hold the recovery key
initprints once.
Install
uv tool install git+https://github.com/MarShaikh/carryon
# or
pip install git+https://github.com/MarShaikh/carryon
Upgrade with uv tool upgrade agent-carryon (the package is agent-carryon;
the command is carryon). Uninstall with uv tool uninstall agent-carryon.
Removing the tool leaves ~/.carryon/ and your Archive where they are.
Getting started
Every command that changes something prints a plan first and does nothing
until you add --apply. Run it without, read the plan, then run it again.
1. See what would move
carryon list
This shows which agents carryon detected on this machine and, for each, what
it would carry in each category: config, capability and knowledge make
up the Setup, and history is the History. Your own excludes and carry are
already applied, so the listing is what a push would really take.
2. Set up the first machine
carryon init # asks where the Archive should live
carryon init --dest ~/Dropbox/carryon # or say so outright
init checks the Destination before it commits to anything. It refuses if an
Archive is already there, and it writes, reads and deletes a probe to prove the
storage works. Then it stores a fresh master key in the macOS keychain, writes
~/.carryon/config.json, and prints your recovery key:
Your recovery key - shown once, never stored by carryon:
ABCD-EFGH-JKLM-NOPQ-RSTU-VWXY-Z234-5672
Put it in your password manager now. The master key that opens the Archive is derived from it. If every machine loses its key, the recovery key is the only way back in. There is no reset.
3. Push
carryon push # the plan
carryon push --apply # do it
The Setup goes up as one sealed object. The History goes up as one sealed object per Session, and only the Sessions that changed since the last push. If the scan recognises a credential, push names the file and carries on.
Pushing to rclone remote carryon:carryon-archive
Setup: pushed, encrypted (Claude Code, OpenAI Codex CLI)
possible secrets in 1 file, carried encrypted:
.claude/settings.json (anthropic-key)
History: 1 Session pushed (23 unchanged), 1 project memory pushed (8 unchanged)
--verbose lists each file and folder behind the summary.
4. Add a second machine
On the machine that is already set up:
carryon pair
It prints a one-time code and the exact command to run on the new machine:
carryon init --join XXXX-XXXX-XXXX-XXXX --dest ~/Dropbox/carryon
carryon pull # the plan
carryon pull --apply # lay the Archive down here
The code is good for one use within 24 hours. The master key travels through the Destination, wrapped under that code, and lands in the new machine's keychain without anyone typing it. Use the same Destination spec the first machine uses.
A pull unions the History, so Sessions from both machines end up on both. It
replaces the Setup after backing up whatever it overwrites to
~/.carryon/backups/.
Pulling from rclone remote carryon:carryon-archive
Setup: restored from machine 'mac-a' (pushed 2026-10-21T09:12:00Z): 12 files written
skipped: ~/.claude/CLAUDE.md - ~/dotfiles/CLAUDE.md holds it (--force writes through)
files it replaced are backed up under ~/.carryon/backups/2026-10-21T09:15:02Z-…
History: 3 Sessions pulled (20 unchanged), 1 project memory pulled (4 unchanged)
diverged: 1 Session - this machine's copy is kept and the Archive's is under ~/.carryon/conflicts/:
3f2a9c1e-… (claude-code)
--verbose lists each Session and file as it lands.
5. Day to day
carryon status
status tells you what is out of step, the way git status does, and moves
nothing. It reads the Archive's catalogue rather than downloading anything,
and each line ends with the command that closes the gap:
status: rclone remote carryon:carryon-archive
History: 3 to push, 2 to pull - run `carryon sync --apply`
Setup: home-mac pushed changes (2026-10-19 09:12) - run `carryon pull --apply` to bring them here
in step with ... means there is nothing to do. --verbose lists the Sessions
and projects behind each count. Nothing runs on its own: carryon has no
scheduler and no background process (ADR-0015).
carryon sync --apply
sync carries the History both ways in one pass: pull first, then push.
That order is deliberate, because pushing first leaves the Archive a round
behind. By default it moves only the History. Your Setup changes less often and
deserves a deliberate step:
carryon push --apply --category config,capability,knowledge # after changing settings or skills
carryon pull --apply --category config,capability,knowledge # to take them on another machine
sync never merges a Session that two machines extended at the same time. It
keeps your copy, files the Archive's under ~/.carryon/conflicts/, and says
so. Running it again changes nothing until you decide.
If a machine loses its key
A new laptop, a wiped keychain, a machine restored without its keychain: the cure depends on whether another machine still holds the key.
- Another machine still has it: run
carryon pairthere andcarryon init --join CODEhere, as in step 4. If this machine still has itsconfig.json, joining keeps it exactly as it was, so--destcan be left off. - No machine has it: type the recovery key back in.
carryon init --recover
carryon asks for the key at a prompt that does not echo. It never takes the key
as an argument, and without a terminal it refuses. It keeps the key only once
the key has opened the Archive's Index. A key from another Archive, or a typo
that still parses, is asked for again with nothing stored, and Ctrl-D stops.
A right key says so first: recovery key accepted, then the Destination
check, then recovered as NAME. A pairing code does the same with
pairing code accepted.
Both ways keep a config.json this machine still has. They use that file's
Destination and machine name and leave the file exactly as it was, your excludes
and handpicked paths included. A --dest or --machine that disagrees with it
is refused before anything is stored, and before a pairing code is spent.
What moves
| Agent | Setup | History |
|---|---|---|
Claude Code (~/.claude) |
settings.json, settings.local.json, keybindings.json, CLAUDE.md, skills, subagents, slash commands, the plugin and marketplace manifests |
Sessions and per-project memory under projects/ |
Codex CLI (~/.codex) |
config.toml, AGENTS.md, memories/ |
rollout transcripts under sessions/ |
Cursor (~/.cursor) |
cli-config.json with its authInfo removed, rules, commands, subagents, the skills sync manifest |
— |
~/.agents |
.skill-lock.json |
— |
Some things are carried as a pointer rather than as the thing itself. Plugins
travel as their manifest, not as copies. Skills installed under ~/.agents
travel as the lock file they are reinstalled from. A Claude Code skill that is
a symlink is re-resolved on the other machine, and one that is a real directory
is carried.
Some things are left out on purpose: caches, telemetry, live run state, prompt
recall (history.jsonl), and machine identity (~/.claude.json). Each one,
and the reason for it, is in the adapter under carryon/adapters/.
carryon doctor reports anything in an agent's directory that no adapter
recognises, which is how a vendor's layout change gets noticed.
The categories --category takes are config, capability, knowledge
(those three together are the Setup) and history.
Choosing what moves
The defaults move everything above. To change that, edit
~/.carryon/config.json:
{
"version": 1,
"destination": "~/Dropbox/carryon",
"machine": "work-laptop",
"excludes": [".claude/keybindings.json", ".cursor/*"],
"carry": ["~/.config/ghostty/config", "~/bin/my-scripts"]
}
excludesremoves whole Items an adapter declares. Each pattern is matched case-sensitively against the Item's path relative to$HOME, as the adapter declares it (.claude/keybindings.json,.cursor/rules), so the same config excludes the same things on every machine. A pattern that matches nothing is reported, so a typo does not quietly exclude nothing.carryadds paths no adapter knows about. They always join the Setup, never the History, and they must sit inside$HOME.~/.carryonitself is refused, because the master key can live there.
carryon checks this file strictly on every command. An unknown key is an error, not a setting that is silently ignored.
Commands
| Command | Effect |
|---|---|
init |
set up this machine: Destination, recovery key, config. In a terminal it asks where the Archive should live; --dest SPEC says so outright. --join CODE pairs with an existing Archive instead, and --recover takes the recovery key back when no paired machine is left |
push |
push this machine's Snapshot, encrypted: the Setup as one sealed object, the History as changed Sessions only — never one the Archive is ahead on. Needs the master key |
pull |
lay the Archive down here: union the History, replace the Setup after a backup |
sync |
carry the History both ways in one pass: pull what the Archive has, then push what it lacks — pull first, on purpose, and it never merges a Session two machines extended at once |
status |
say what is out of step with the Archive and which command closes it, read off the Index without downloading anything; it moves nothing. --verbose lists each item |
resolve [PATH...] |
end a divergence in per-project memory after you have merged the Archive's filed copy by hand; with no paths, list what is filed |
pair |
mint a one-time code that hands another machine the master key, via the Destination |
list |
show detected agents and what would be captured |
doctor |
check for layout changes: entries no adapter recognises |
capture --out DIR |
capture a Setup into a local directory, in the clear, to read or diff: no Destination, no key, no History |
encrypt / decrypt |
encrypt any file with a passphrase — a standalone cipher, nothing to do with an Archive |
push, pull, sync, resolve and capture print a plan and change nothing
without --apply. --help on a subcommand lists its own flags (--agent,
--category, --map, --force, --verbose).
pull --map OLD=NEW rewrites an absolute path outside $HOME on the way in,
for a project that lives at /Volumes/work/... on one machine and somewhere
else on the other. Paths under $HOME need no map: carryon rewrites them to a
machine-neutral form on push and expands them against the new home on pull.
What carryon will not do to your files
A pull never deletes anything under $HOME, and carryon never writes through a
path another tool already holds — a dotfiles symlink, a second hard link, a
name this machine will not answer about. Such a path is skipped and named in
the report rather than silently overwritten. --force writes through a symlink
or hard link on the Setup half; it still refuses a path no adapter here
declares, one landing in carryon's own state, and anything that is not an
ordinary file.
Before a pull overwrites a file, it backs that file up under
~/.carryon/backups/<timestamp>/.
Per-project memory (MEMORY.md and the notes beside it) gets edited in place,
not only appended to, so carryon remembers the version of each memory file this
machine and the Archive last agreed on: its Base. If only one machine has edited
a file since then, that edit travels, on push and on pull. A pull that writes
another machine's edit over yours backs your copy up first. If both machines
edited the same file, the pull keeps yours and files the Archive's under
~/.carryon/conflicts/<project>/. Merge it by hand, run
carryon resolve <path> --apply, and the next push carries the merge. A memory
file that was edited before upgrading has no Base yet. It gets one the first
time a push or pull finds it in step with the Archive. A file still out of step
then is filed by pull --apply, and needs that same merge and resolve.
Where carryon keeps things
On each machine, everything lives under ~/.carryon/:
| Path | What it is |
|---|---|
config.json |
the Destination, this machine's name, excludes and carry |
state.json |
what this machine has seen of each Archive: the highest Index revision (so a rolled-back Archive is noticed) and each memory file's Base |
backups/ |
every file a pull overwrote, one directory per pull |
conflicts/ |
the Archive's copy of anything two machines changed at once, waiting for you |
master.key |
only on a machine with no OS keychain; on macOS the key is in the keychain under service carryon |
At the Destination, everything sits under carryon/. The Index records which
Sessions and Setups the Archive holds, Setups are kept one sealed object per
machine, and Sessions are sealed objects under names the Destination cannot
read.
Destinations
carryon holds no credentials for a Destination; it borrows whatever you already have. A synced folder needs no account at all. Everything in the Archive is sealed wherever it lands — encrypted, and checked before it is opened — so a pull refuses an object that was edited at the Destination. What the Destination can still see is the shape: how many objects, how large, when they changed, and the machine names in each Setup's key. A pull also refuses an older Setup served back in place of the current one, which an encrypted object alone cannot rule out.
An Archive pushed before the Setup was sealed holds it as a plaintext tree, and
a pull refuses that tree by name. One full carryon push --apply replaces it
with the sealed object and removes the plaintext files. On a git Destination
they stay in the repository's history.
| Spec | Where the Archive lives |
|---|---|
/path, ~/path, dir:PATH |
a directory — including a synced one (Dropbox, Drive, Syncthing) |
git:URL, git@..., ssh://..., anything ending .git |
a private git remote |
rclone:remote:path |
anything rclone reaches: S3, R2, B2, ... |
With a terminal on both ends, init offers what it found here — synced
folders, git if it can already authenticate, rclone remotes you already have —
and a short list of services it can set one up for: Cloudflare R2, Amazon S3,
Google Cloud Storage, SFTP. It asks that service's few fields, hands them to
rclone config create, and offers to create the bucket, asking first because
that is a billable resource in a region and under a name carryon has no
business choosing. The credential passes through to rclone and is never kept.
Without a terminal — over SSH with no tty, in CI — init prints the
candidates and exits. A script names its Destination with --dest.
Then it asks the Destination two questions. Whether an Archive is already
there: setting up over one without --join would mint a second recovery key
that does not open what is already stored, and nothing afterwards tells the two
keys apart — so it refuses and says how to pair instead. And whether
write, read and delete really work, using a probe of random bytes under a
random name that it reads back and deletes. On an rclone Destination the probe
refuses to write into a bucket that is not there rather than let the upload
create one. A directory Destination that does not exist yet is created, the
way the first push always created it — so the spelling of --dest ~/Dropbox
is yours to get right; a typo is a working Archive in a folder no sync client
watches. A git Destination is not probed at all: every write there is a commit
that stays in history, and git refuses a push that does not land, so the first
carryon push is its write test. Both questions come before a key is minted
or a config written, so a refusal costs nothing and can be re-run. Neither
says whether the storage is private — no check can, which is why that stays
your call.
Prefer a Destination that forgets when you have the choice. A pairing code is deleted from the Archive once it is used, but on a git Destination that delete is a commit, and the wrapped master key stays in the history for anyone who can clone the repository.
When carryon refuses
carryon would rather stop and say why than guess. Each refusal names its cure; these are the ones a new user meets most.
| It says | What to do |
|---|---|
| this machine holds no master key | carryon init --join CODE --dest SPEC with a code from carryon pair, or carryon init --recover if no machine has the key |
| there is already an Archive at ... | you are setting up a second machine: pair it rather than starting a new Archive |
| could not read the master key via security | the keychain is locked or unreachable; the key may well still be there, so fix the keychain rather than pairing again |
| the stored master key ... is not the hex carryon wrote there | put the file back from a backup, pair again, or remove it and run carryon init --recover |
| wrong or expired code | the code was mistyped or is past its 24 hours; run carryon pair again |
| refusing to push: the Archive's Index is at revision ... | the Archive has gone backwards since this machine last saw it; restore the newer copy, or follow the message to accept it deliberately |
| Skipped ... rather than overwrite the Archive's copy | carryon pull --apply first, then merge anything filed under ~/.carryon/conflicts/ and carryon resolve it |
Adding an agent
Add a module to carryon/adapters/ defining ADAPTER, and list it in
MODULES. The engine reads the declaration.
from .base import CONFIG, Adapter, Excluded, Item
ADAPTER = Adapter(
key="my-agent",
name="My Agent",
detect=".myagent",
verified_against="My Agent 3.2, macOS 15",
platforms=("darwin",),
items=(
Item(".myagent/config.json", "myagent/config.json",
"file", CONFIG, "settings", required=True),
),
exclude=(Excluded(".myagent/token", "CREDENTIAL", "log in again"),),
known_entries=("config.json", "token", "cache"),
)
Set verified_against to a version you actually checked. known_entries
lists every top-level name you know about — anything else is reported by
doctor, which is how a layout change gets noticed. An agent whose Sessions
sit in a shape the History engine has no layout for carries its Setup only;
adding that shape is engine work rather than a declaration.
Platform support
Every adapter is declared for darwin and was checked there; only the
~/.agents one also claims Linux. The test suite runs on macOS and Ubuntu in
CI, which shows carryon itself works on Linux; it does not show where an agent
keeps its files there, which is what an adapter's platform claim is about.
Nothing has been run on Windows.
doctor tells you when you are on a platform an adapter does not cover, and an
adapter's verified_against records the version and OS it was actually checked
on rather than a guess.
Tests
uv run --extra dev pytest -q
python3 run_tests.py # no pytest required
Both runners run the same suite and have to agree.
A History is your transcripts, so what the suite does not cover is worth
knowing before you point carryon at them. No agent binary is involved anywhere:
the suite builds agent directories itself, so what is verified is carryon's
behaviour against the layouts each adapter records — not against a live agent,
and not against a vendor who has since reorganised. The keyring runs through
its file backend with the real keychain pinned out, so security(1) and
secret-tool are covered by the arguments carryon builds for them rather than
by use. openssl and git are the real binaries, and a full journey — init,
push, pair, join, pull, push back — runs against both a directory Destination
and a bare git repository; rclone is a stand-in, for setting a Remote up as
well as for reaching one, so the Provider flow is checked against the argv
carryon builds and against rclone's own backend source — never against a live
bucket. Two carryon runs against one Archive at the same time is not tested.
Licence
MIT. Credential detection rules ported from
entangle
(MIT, © 2026 Gowtham Sai Yadav); see LICENSE.
Metadata
Release files for agent-carryon 0.2.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| agent_carryon-0.2.0.tar.gz | 593.0 kB | With provenance | Details for agent_carryon-0.2.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| agent_carryon-0.2.0-py3-none-any.whl | Python 3 | none | any | Details for agent_carryon-0.2.0-py3-none-any.whl |
Total release size: 874.3 kB
Release files / agent_carryon-0.2.0.tar.gz
| Download URL | agent_carryon-0.2.0.tar.gz |
|---|---|
| Size | 593.0 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
187c1259b23c3cd5bda6ded3b47e82161543ba3f19386c2f19654fb0fc42694a
|
|
BLAKE2b-256 checksum How to use checksums |
85809c14e3cbf2c8f11a4e3a52685be1b00f67508ea4de1a045e8037164e35dc
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 26, 2026.
Transparency logRelease files / agent_carryon-0.2.0-py3-none-any.whl
| Download URL | agent_carryon-0.2.0-py3-none-any.whl |
|---|---|
| Size | 281.4 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
ad97349927ad0a168802f94c400389a124480c4bcf011e3450a3805c1815a051
|
|
BLAKE2b-256 checksum How to use checksums |
d3fb3c2fc2b08b00637d8183ea498f02cbf6e48d5b79d445a41a2f041b553c65
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 26, 2026.
Transparency logSecurity for agent-carryon 0.2.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for agent_carryon-0.2.0.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 26, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.