agent-sync

Documentation

agent-sync keeps one copy of every config you share and puts it where each agent expects to find it. This page is the reference: what lives in the Commons — the canonical ~/.agents/ directory — how each family reaches each agent, and what the tool will and will not touch.

Install

# prebuilt binary for macOS, Linux and Windows — no toolchain, no postinstall script,
# so it installs offline and inside a sandboxed CI
npm install -g agent-sync-sh

# or the same binary as a PyPI wheel — no Python code, just the binary
pip install agent-sync-sh

# or build from source — needs Rust 1.97 or newer
cargo install agent-sync-sh

# or try it first, without installing anything
npx agent-sync-sh doctor
uvx agent-sync-sh doctor

# or on macOS and Linux, from the Homebrew tap in the repo itself
brew tap agent-sync-sh/tap https://github.com/agent-sync-sh/agent-sync
brew trust agent-sync-sh/tap
brew install agent-sync

The npm package is a small launcher; the real binary ships in one platform package per target, declared as optional dependencies so npm installs exactly the one matching your machine. On a platform with no prebuilt binary you get a message naming what it looked for and pointing at cargo install, rather than a missing-file crash. The same design is why npx works: there is no postinstall step and nothing to fetch beyond the binary itself, and doctor only reads — it reports the agents this machine has without creating a Commons.

The PyPI wheels contain no Python code at all — each one carries the same compiled binary in the wheel's scripts/ directory, which is how pip install and uvx put a working agent-sync on PATH with no interpreter shim in between. There is one wheel per platform, so pip resolves the right binary the same way npm picks the right optional dependency. On a platform with no prebuilt wheel you still get a clean install and a command that names your platform and points at cargo install, rather than pip's bare no matching distribution found — the same courtesy the npm launcher extends.

The Homebrew tap lives in the agent-sync repository itself rather than a separate homebrew-agent-sync one, which is why it is tapped by URL rather than by the short brew tap agent-sync-sh/agent-sync form. It installs the same prebuilt binary the release attaches to its GitHub Release, verified against the published SHA256SUMS.txt; the formula is regenerated by CI on every tag, so it can only ever point at assets that exist. Homebrew 6 refuses to load a third-party tap until you trust it, hence the brew trust line.

On Windows, creating symlinks requires Developer Mode (Settings → System → For developers) or an elevated shell; sync says exactly that when it cannot link.

First five minutes

$ agent-sync init
Created the Commons at ~/.agents
  skills/
  commands/
  agents/

Found 6 agents: claude, codex, opencode, pi, oh-my-pi, gemini

3 skills not in the Commons:
  claude/skills/my-workflow
  claude/skills/review-loop
  pi/agent/skills/notes
  take one with `agent-sync adopt <path>`

4 MCP servers not in the Commons: serena, xapi, node_repl, computer-use
  take them all with `agent-sync mcp adopt --all`

init answers the question a used machine actually poses: not what agent-sync does, but what of your existing config it could take over. Adopt what you want shared, then sync.

$ agent-sync adopt ~/.claude/skills/my-workflow
adopted my-workflow from claude into the Commons (skills)

$ agent-sync mcp adopt --all
adopted serena from claude
  scoped to claude — remove the allowlist in agent-sync.toml to share it

$ agent-sync sync

adopt is one verb; where the path lives picks the mechanic. A real config inside an agent's directory is moved into the Commons with a link left behind; a path inside a git repo becomes a Sourced entry — the Commons links out to it and the repo keeps the truth; anything else is copied in. --dry-run names the mechanic before anything moves. If the Commons already holds that name with identical contents it collapses the duplicate into a link; if the contents differ it refuses and explains, because choosing which side to discard is not agent-sync's call.

The Commons

~/.agents/
├── skills/<name>/         a directory per skill
├── commands/<name>.md     Claude-dialect markdown
├── agents/<name>.md       Claude-dialect markdown
├── AGENTS.md              your shared instructions
├── mcp.json               the standard mcpServers shape
└── hooks/<Event>.toml     [[hook]] entries: command, matcher, timeout

The Commons lives at ~/.agents/. It holds ecosystem-standard content only — nothing agent-sync-specific lives there, which is what lets other tools read it directly, as opencode, oh-my-pi and Hermes already do. And it is exactly that — a commons, not agent-sync's alone: other tools keep their own files there, and doctor names entries that aren't agent-sync's and leaves them be. Tool configuration lives separately in $XDG_CONFIG_HOME/agent-sync/ (default ~/.config/agent-sync/).

The six families

Skills, commands, agents
Symlink fan-out. One canonical relative link per entry in each agent's directory; links of ours that dangle are pruned; anything else is left alone. Agents that read ~/.agents/skills natively — Codex, opencode, oh-my-pi, Gemini CLI, Cursor — get no skill links at all, and where Codex or Cursor still reads its old fan-out directory, sync prunes our now-duplicate links from it.
Instructions
Three mechanisms, because agents differ in kind. A plain symlink where the whole file can be ours (Codex, opencode, pi, oh-my-pi, Gemini CLI, Windsurf). An import line for Claude, whose CLAUDE.md holds your own content, so agent-sync only ensures @~/.agents/AGENTS.md is present — additive and idempotent. A rules-dir link for Roo, which globs a directory.
MCP servers
Rendered into each agent's dialect and key-merged. See below.
Hooks
Command-hooks declared once in hooks/<Event>.toml and merged into Claude, Codex and Gemini by command string: agent-sync owns exactly the array elements whose command matches a Commons hook, and other tools' hooks in the same event array survive untouched. Codex trust hashes are never written — approving code execution stays your decision.

A file another tool already owns — claude-mem's boilerplate in opencode's AGENTS.md, say — is a Conflict: reported with the remedy named, never overwritten.

Ownership, without a state file

Everything agent-sync writes must be recognisable as its own on a later run, with no database to consult. Three identities cover every mechanism.

Link identity
A symlink resolving into the Commons is ours — canonicalised, and pruned when it dangles. A link resolving anywhere else is Foreign and is never touched.
Name identity
An entry key-merged into a shared file is ours when its name appears in the Commons; for hooks the command string is the name. Unknown names in the same file are Foreign. One consequence is deliberate: sync never deletes an MCP server, because a name that vanished from the Commons cannot be told apart from one you added by hand. Removal is agent-sync mcp remove.
Marker identity
A wholly generated file carries one comment line saying so. That is what makes pruning safe: removing a marked file returns the machine to a state agent-sync created. An unmarked file at the same path was written by somebody else.

Reading status

StateMeans
linkedA canonical link onto the right Commons entry. Nothing to do.
missingThe Commons has it; this agent does not, yet.
staleOur link, but not in canonical form — it will be rewritten.
danglingOur link, pointing at a Commons entry that is gone. It will be pruned.
variantA real file or directory shadowing a Commons entry, on purpose. Preserved.
variant-identicalSame, but byte-identical to the Commons copy — a duplicate you can collapse.
foreignNot ours. Reported, never modified.
conflictAnother tool owns a file agent-sync would otherwise write.
Exit codes: 0 clean, 1 error, 2 something actionable. A Variant that diverges and a Foreign entry are settled states, not work, so neither makes the exit code 2.

MCP servers

One mcp.json in the standard shape is rendered into each agent's dialect: Codex takes TOML tables with http_headers; opencode wants local/remote with the command flattened into one array and env renamed environment; Gemini distinguishes SSE from streamable HTTP by which URL key it uses; Windsurf calls it serverUrl.

# ~/.config/agent-sync/agent-sync.toml
[mcp.serena]
agents = ["claude", "codex"]      # default: every capable agent

[mcp.node_repl.tweaks.codex]
startup_timeout_sec = 120         # reaches Codex alone

Secrets are written as ${env:VAR} in the Commons, resolved from the environment at sync, and redacted from every line of output — so the Commons stays committable. If a resolved secret would land in a file others can read, agent-sync says so.

mcp disable parks a server without retyping it: the definition and its rules stay in the Commons, renders are removed everywhere, and mcp enable restores them.

agent-sync mcp adopt claude/serena reverse-translates an existing server into the Commons, turning agent-specific knobs into Tweaks. It proves the result first: if re-rendering it would not reproduce what you already have, it refuses rather than reporting a success that the next sync would undo.

CLI

CommandDoes
initCreate the Commons and report what could be adopted.
adopt <path>Take a path under management; where it lives picks the mechanic — absorb, Sourced, or copy. --dry-run names it.
syncFan the Commons out to every installed agent — planned in full before the first write. --dry-run prints the plan.
statusWhat is in sync, what is not, what is not ours. --json.
doctorInstalled agents, Commons hygiene, Sourced entries with missing clones marked, and .agents Protocol surfaces attributed by name.
revert <agent>Remove everything agent-sync put into one target — links, merged MCP and hook keys, rendered files. Refuses until the target is disabled.
mcp listCommons servers and their state in every agent. --json.
mcp adopt<agent>/<server>, or --all.
mcp removeDelete a server from the Commons and from every agent.
mcp enable | disablePark a server or restore it — the definition and its rules stay in the Commons either way.

agent-sync.toml

Optional, in $XDG_CONFIG_HOME/agent-sync/ (default ~/.config/agent-sync/agent-sync.toml). Absent means defaults. It never lives in the Commons, so the Commons stays ecosystem-standard content only. The lock — agent-sync's only machine state — lives in $XDG_STATE_HOME/agent-sync/ (default ~/.local/state/agent-sync/).

[targets]
cursor = false                    # leave this agent alone entirely

[custom.myagent]                  # an agent the registry does not know
root = ".myagent"
skills = ".myagent/skills"

[mcp.heavy-server]
agents = ["codex"]

Many machines

agent-sync does not sync across machines, deliberately — that would mean building conflict resolution, and git already won that argument. The Commons is an ordinary directory, so version it:

# as a git repo
cd ~/.agents && git init && git add . && git commit -m "my agent config"

# or with chezmoi
chezmoi add ~/.agents ~/.config/agent-sync

On the second machine, clone it and run agent-sync sync.

Vocabulary

Commons
The canonical directory ~/.agents/ holding the single real copy of everything agent-sync syncs — a shared commons other tools also live in; agent-sync is a tenant, never the landlord.
Target
One agent's config surface that receives fan-out.
Fan-out
One Commons entry materialised into every Target as a symlink.
Native
An agent that reads the Commons directly and therefore needs no fan-out.
Variant
A real file or directory in a Target that intentionally shadows the Commons copy for that agent alone. First-class and preserved, never clobbered.
Adopt
Taking a path under agent-sync's management with one verb; where the path lives picks the mechanic — absorbed into the Commons with a link left behind, made a Sourced entry when it lives in a git repo, copied in otherwise.
Sourced
A Commons entry that is a symlink out to its Source — an outside path, typically in a git repo, holding the content's single real copy. The Commons keeps the pointer; the Source keeps the truth.
Revert
Removing everything agent-sync put into one Target, as a deliberate offboarding act. Refused while the target is still enabled, so a later sync cannot redo what revert undid.
Foreign
A link, file or server entry in a Target that agent-sync does not own. Never touched, listed by status.
Conflict
A Foreign file occupying a destination agent-sync would otherwise write — reported with the remedy named, never overwritten.
Render
Translating a Commons entry into one agent's native format and merging it into that agent's config file.
Managed
An entry whose name appears in the Commons: agent-sync owns it in every targeted config, and the Commons wins.
Disabled
A Managed server whose Commons definition carries "disabled": true — defined but rendered nowhere, so enabling restores it everywhere without retyping.
Protocol surface
A .agents Protocol path at the Commons root — tasks/, memories/, models.json, system-prompt.md — that doctor attributes by name and agent-sync never touches.
Tweak
A per-agent addition merged into one agent's rendering, declared in agent-sync.toml, never in the Commons.
Marker
The one-line comment in every generated file, making it recognisable as agent-sync's without a state file.