Several coding agents, one repository, no collisions — and each one can see what the others are doing.
agent-sync is an agent skill (plus a Claude Code plugin) that gives concurrent
coding agents a coordination plane: leases with a TTL, race-free id reservation, a run
journal, a cross-repo signal feed and a generated board.
- The problem
- What you get
- Requirements
- Install
- Update
- Set up a project
- Everyday use
- Configuration
- Backends
- Enforcement hooks
- Where it plugs into task-pipeline
- Limits, stated plainly
- Troubleshooting
- Uninstall
- Develop and verify
When more than one agent works a project at the same time, the coordination substrate most teams already have — a decisions log, a roadmap, a board, per-repo task files — stops being enough. Every one of those is a file edited by hand. That works for people taking turns and fails for agents working at once:
| What goes wrong | Why |
|---|---|
| Two agents mint the same decision id | "Next free id" is a line in a file; reading it is not reserving it |
| A claim blocks a task forever | A role name is not a holder, and it has no expiry |
| Two agents start the same task | Git shows what was committed, never what is in flight |
| An agent is blocked but not informed | Knowing a task is taken is not knowing who has it or what they touch |
| Merge conflicts on every shared register | Everyone writes the same three files |
| A cross-repo dependency is never noticed | Filing one notifies nobody |
agent-sync closes exactly those, and nothing else.
Git is the record plane. The cloud is the coordination plane.
A fact that must survive is written to git first and referenced from the cloud. A fact about who is doing what right now lives in the cloud and expires. No cloud object is ever the only home of a durable fact, so your single-source-of-truth rules stay intact.
The knowledge base never decides a lease. It cannot: measured against a real
instance, twelve concurrent appends to one document returned twelve successes and left
three lines. Exclusion comes from something that genuinely has compare-and-swap — an
atomic file create on one machine (leaseBackend: "local"), or a pushed git ref across
machines (leaseBackend: "git"), where the remote's non-fast-forward rejection is the
CAS. Id reservation still replays the log, which is safe because allocation is positional
and every reader computes the same answer.
- Leases with a TTL — claim a task, renew automatically, steal an expired one.
Exclusive across machines with
leaseBackend: "git"; the tool always states which guarantee you have rather than implying the stronger one. - The claim written through to the roadmap — one row, one cell, refused on ambiguity, and restored verbatim on release. Closing a task stays yours.
- Awareness, not just exclusion —
statuslists every other run's live holdings, so an agent learns who holds a task and what they are touching, instead of only that it is taken. - Race-free id reservation — positional allocation over the log, so two agents cannot be handed one number.
- A run journal — what each run did, with commits, gate results and evidence.
- A cross-repo signal feed —
filed → accepted → delivered → closed.statussurfaces what landed since this run last looked, watermarked per run so it stays quiet until something actually changes. - A generated board — machine-written, commit-stamped, and it refuses to overwrite a page a human took over.
- Enforcement hooks for Claude Code that deny an edit to a guarded register file, or a commit staging one, without a live lease.
| Requirement | Why | Check |
|---|---|---|
| python3 ≥ 3.9 | the coordinator is one stdlib-only script — HTTP included, nothing to pip install |
python3 --version |
| git | the record plane, and the cross-machine lease store | git --version |
| bash | the four Claude Code hook scripts | bash --version |
| Node ≥ 18 | only for the npx @ssheleg/agent-sync installer |
node --version |
| task-pipeline | agent-sync supplies stages, it does not define them — without it status prints one line and stops |
npx sshlg-skills install |
| A knowledge-base instance (optional) | the shared record, awareness and board; without one the fs backend keeps leases but loses cross-agent visibility |
— |
npx @ssheleg/agent-sync installClaude Code gets the plugin; every other agent gets the skill through the
skills CLI. The duplicate plain copy in
~/.claude/skills/ is pruned afterwards, because that shadow silently serves a stale
skill over the installed plugin — one channel per agent is the rule.
Restart Claude Code after installing, so it picks the plugin up.
Other install routes
Track main from GitHub instead of the npm release:
npx github:appvillis-com/agent-sync installClaude Code only, no skills CLI:
npx @ssheleg/agent-sync install --claude-onlyPick which agents the skills CLI installs for:
npx @ssheleg/agent-sync install --agent cursor,codexOr add the plugin by hand — the full <name>@<name> form is required:
claude plugin marketplace add appvillis-com/agent-sync && claude plugin install agent-sync@agent-syncUpdate the whole family — one package, every agent. A bundle with one member current and the rest stale is a combination nobody tested:
npx sshlg-skills update # installed but behind — updates everything
npx sshlg-skills install # nothing installed yet
npx --yes sshlg-skills@latest list # what the current release of each member isRestart your agent afterwards: skills and hooks load at session start, so the session that updates is not the session that gets the new ones.
Updating this one member only
agent-sync itself — update every channel you installed, then restart Claude Code:
claude plugin marketplace update agent-sync && claude plugin update agent-sync@agent-sync && npx --yes skills update agent-sync --global --yesRe-running the installer works too, but pin @latest or npx may serve you its cache:
npx @ssheleg/agent-sync@latest installCheck what you are actually running — the plugin and the skill must report the same version, and a mismatch means one channel is stale:
claude plugin list | grep agent-sync
python3 ~/.claude/plugins/cache/*/agent-sync/*/skills/agent-sync/scripts/agent_sync.py --versionIts dependencies — task-pipeline (and the rest of the same family) come from one
installer, which also prunes the shadow copies:
npx sshlg-skills installNothing else to update: the coordinator is stdlib-only python, and the npm package has zero runtime dependencies.
Initialisation is the first command, and it asks a question rather than guessing.
/agent-sync init
The agent asks where coordination state should live — a knowledge cloud, or local files — and, for the cloud, the instance URL. Then it writes two files:
| File | Holds | Committed? |
|---|---|---|
.claude/agent-sync.json |
shape — backend, TTLs, guarded files, registers, gates | yes |
.env.agent-sync |
identity — instance URL, token, collection id | no — mode 600, added to .gitignore |
The token line is written empty. Creating the API token in your own instance and pasting it into that line is your step, and it stays yours: the tool never asks for a token in chat, never echoes one, and never passes one as a command-line argument.
Load the environment before running agents:
set -a && . ./.env.agent-sync && set +aThen create the container the coordination log lives in, once per project, and paste the
id it prints into AGENT_SYNC_OUTLINE_COLLECTION:
/agent-sync bootstrapVerify the setup — idempotent, repairs what is missing, and names exactly one next action:
/agent-sync statusIn an agent session you use the slash command (/agent-sync claim ASC-072); the same
commands run directly against the coordinator script, which is what the hooks and CI do:
python3 "$SKILL_DIR/scripts/agent_sync.py" <command>| Command | Does |
|---|---|
init |
Run first. Ask where state lives, write config + gitignored env file, print your step |
status |
Inspect, repair, report — including other runs' leases and signals new since you last looked |
bootstrap |
Create the cloud container and print the id to paste into the env file |
acquire <KEY> |
Take the lease on a task id. Prints won, or lost <holder> |
renew <KEY> |
Extend the lease. In Claude Code the PostToolUse hook does this for you |
release <KEY> |
Give the lease back. Always, including on failure |
reserve <REG> |
Reserve the next id in a register (DEC, OQ, DEP, …). Prints the id |
release-id <REG> <ID> |
Return an id you did not end up writing to git |
journal <text> |
Append one line to this run's journal |
signal <DEP-ID> <state> |
Move a cross-repo dependency: filed/accepted/delivered/closed/refused |
guard <path> |
May this run write that path? Exit 0 = yes, 2 = no |
board |
Regenerate the read-only board and the mirror from git |
whoami |
Print this run's id and its held leases |
The shape that matters:
acquire → do the work → release
Never skip release, including when the work failed. An abandoned lease blocks the task
until its TTL expires, and the next agent cannot tell "in progress" from "crashed an hour
ago". A lease is a promise to come back.
The lease is not the claim. The lease says who holds the task now and expires; the
durable claim is the tag in git ([name], todo (claimed: <role>)). acquire writes
that tag through and release clears it, so one fact keeps one home.
.claude/agent-sync.json — committed, validated against
agent-sync.schema.json, starting point in
agent-sync.example.json:
| Key | Meaning |
|---|---|
backend |
outline or fs (required) |
leaseTtlSeconds |
how long a lease survives without a renew (default 2700) |
renewIntervalSeconds |
how often a live run renews (default 300) |
gated |
whether runs may be recorded as enforced at all |
idRegisters |
register → the git file that owns it, and its "next free id" pattern |
guardedFiles |
registry files no run may edit without a live lease |
claimTags |
file → the durable claim tag acquire/release writes through |
gates |
commands the pipeline stages run as gates |
mirror |
which git files are rendered into the read-only mirror |
.env.agent-sync — gitignored, mode 600:
AGENT_SYNC_BACKEND=outline
AGENT_SYNC_OUTLINE_URL=https://<your-instance>
AGENT_SYNC_OUTLINE_TOKEN= # you fill this line, nobody else
AGENT_SYNC_OUTLINE_COLLECTION= # printed by `bootstrap`
Never write a host name or a token into the config, a test, an example or a commit. If an agent offers to handle a token for you, that is the wrong answer.
A submodule's config declares only its own registers. Cross-repository facts belong to the parent repository; a service repo listing the parent's decision register is a configuration defect.
Two settings, two jobs. backend chooses the record plane — where the log, the
signals and the board live. leaseBackend chooses what actually decides a lease. The
knowledge base is never the second one: measured against a real instance, twelve
concurrent appends to one document returned twelve successes and left three lines.
backend — the record plane |
What it gives |
|---|---|
outline |
Outline, hosted or self-hosted. Every repository and machine reads one plane: shared awareness, cross-repo signals, the board |
fs |
Local files. No credentials, and no visibility to an agent on another machine |
leaseBackend — the lease |
Guarantee |
|---|---|
git |
Exclusive across machines. The remote's non-fast-forward rejection is a real compare-and-swap |
local (default) |
Exclusive on this machine, advisory across machines. An atomic file create |
status, acquire and check all state which of the two you have, in the same words,
because a lease that is not actually exclusive is worse than none: the other agent has
stopped checking. runs recorded: gated follows the lease mode — never the record plane.
Adding one: read
references/adapter-contract.md.
Installed with the Claude Code plugin. Every hook exits immediately in projects without
.claude/agent-sync.json, so installing globally changes nothing elsewhere.
| Hook | Runs | Effect |
|---|---|---|
SessionStart |
startup, resume | status — the board summary, other runs, one next action |
PreToolUse |
Edit/Write/MultiEdit/NotebookEdit, and git commit |
Denies the edit (exit 2) when the path is guarded and this run holds no lease; a git commit is checked against every staged path |
PostToolUse |
every tool call | Throttled renew — touches the network at most once per renewIntervalSeconds |
SessionEnd |
session end | Releases every lease this run holds |
Details and removal:
references/hooks.md.
agent-sync supplies stages; it does not define them. It binds to task-pipeline's
stages 0, 3, 4, 5, 9 and 10 — lease before the brief is committed, reserve ids before
they reach git, register file ownership for parallel groups, signal and regenerate the
board at docs, release everything at acceptance. Wiring:
references/pipeline-binding.md.
- Hooks are Claude Code only. On Cursor, Codex and the rest there is no
PreToolUse, so nothing blocks a guarded edit; the same checks run as a self-check and the run is recordedungated. Read the board's column rather than assuming. - Ordering, not clocks. Document order decides who holds a lease; timestamps only expire one. Agents' clocks differ and the protocol does not depend on them.
- A reserved id that never reaches git is reported, not reclaimed. A half-written decision on a branch is not an unused number.
fsis not cross-machine. As a record plane it is invisible to agents on another host, and the defaultlocallease is a mutex on this one. SetleaseBackend: "git"when a fleet spans machines; the tool says which guarantee you have rather than implying the stronger one.
| Symptom | Cause and fix |
|---|---|
task-pipeline is not installed and status stops |
Intentional — there are no stages to bind to. npx sshlg-skills install |
⚠ this lease is advisory, not enforced |
gated: false in the config, or a leaseBackend that is neither local nor git. Fix the mode — an unknown one claims nothing on purpose |
lease: local — advisory across machines |
Expected on the default. Set leaseBackend: "git" (and a reachable leaseRemote) when agents run on more than one machine |
Every acquire reports lost |
Check the holder in status. If the log itself is unreadable, acquire raises instead — that is a parse failure, not a race |
| Guarded edit blocked in Claude Code | Working as designed: acquire the key first, or unstage the file |
| Guarded edit not blocked | You are not on Claude Code. Run guard <path> yourself; the run is ungated |
AGENT_SYNC_OUTLINE_COLLECTION is not set |
Run bootstrap and paste the printed id into .env.agent-sync |
An HTTP 400/403 from the backend |
The response body is surfaced verbatim — read it; a bad collection id and a bad token look nothing alike |
| A stale skill after updating | Two channels serving one skill. Delete ~/.claude/skills/agent-sync and keep the plugin |
| The board refuses to write | A human took the page over (no generated marker on line 1). Reported, never overwritten |
claude plugin uninstall agent-sync@agent-sync
npx --yes skills remove agent-sync --global --yesProject files stay where they are; delete .claude/agent-sync.json, .env.agent-sync
and .agent-sync/ if you want the project clean too.
python3 test/validate.py # manifests, version sync, no host/credential leaks
python3 test/validate.py --self-test # the validator must still be able to fail
npm test # both of the aboveWhat ships: one skill (agent-sync), scripts/agent_sync.py (stdlib only), four hook
scripts, the slash command, agent-sync.schema.json, and eight reference contracts the
agent loads on their own trigger rather than by default:
| Reference | Read it when |
|---|---|
adapter-contract.md |
adding or auditing a knowledge backend |
lease-protocol.md |
changing acquisition, expiry, stealing or id allocation |
backend-outline.md |
making any Outline API call, or debugging one |
backend-fs.md |
running without a cloud backend, or explaining degraded mode |
pipeline-binding.md |
wiring pipeline.json, or adding a stage hook |
hooks.md |
installing, debugging or removing the Claude Code hooks |
two-sources.md |
before the first reconcile, or when deciding where a document belongs |
roadmap.md |
configuring claimTags, taking or closing a task, or re-planning a board |
See CONTRIBUTING.md and CHANGELOG.md. Everyone taking part is expected to follow the Code of Conduct. Security reports: SECURITY.md.
agent-sync also ships in the sshlg-skills
bundle, which installs the whole family for Claude Code, Cursor, Codex and 70+
other agents with one command.
MIT © Appvillis