Keep equivalent AI-harness asset files converged on the latest change.
Project description
id: dedd4778-ae3b-5a1f-af39-9f386dfb8857
syncmd
Keep your AI-harness instruction and skill files in sync — automatically, from git.
The problem
Every agentic tool reads its own instruction/context/skill files, but they almost always mean the same thing:
| Harness | Instruction file |
|---|---|
| Claude Code | CLAUDE.md |
| Cross-tool standard | AGENTS.md |
| GitHub Copilot | .github/copilot-instructions.md |
| Cursor | .cursorrules, .cursor/rules/project.mdc |
| Gemini | GEMINI.md |
| Windsurf | .windsurf/rules/rules.md |
| Cline | .clinerules |
| Roo Code | .roo/rules/rules.md |
| JetBrains Junie | .junie/guidelines.md |
| Amazon Q | .amazonq/rules/project.md |
| Kiro | .kiro/steering/project.md |
| Goose | .goosehints |
| Warp | WARP.md |
| Qwen Code | QWEN.md |
| Aider | CONVENTIONS.md |
| OpenHands | .openhands/microagents/repo.md |
| Augment | .augment-guidelines |
| Replit | replit.md |
Run syncmd formats to list every built-in harness label and its mount points. By default
plan/sync cover all formats; restrict with a comma-separated list:
syncmd sync --formats=claude,agents,cursor
A teammate edits CLAUDE.md. Now AGENTS.md, .cursorrules, and the rest silently drift out
of date — and whoever opens the repo with a different tool gets stale guidance. The same
happens to skills (SKILL.md under .claude/, .agents/, .github/) and specs.
syncmd treats a group of equivalent files as one logical asset with several native mount
points, decides which copy is the latest authoritative version using git history plus
your working tree, and propagates it to the rest. Edit whichever file your tool uses; run
syncmd; everything converges.
It is a compiler, not a copier: it figures out which member actually changed since the group last agreed, so a concurrent edit is reported as a conflict instead of being silently clobbered.
Install
# CLI
cargo install syncmd # provides the `syncmd` binary
npm install -g syncmd # downloads the matching prebuilt CLI
# Python SDK
pip install syncmd # provides the `syncmd` module
Building from source:
cargo build --release # CLI at target/release/syncmd
maturin develop -m crates/syncmd-py/pyproject.toml # Python module into the active venv
syncmd requires the target path to be inside a git repository — the whole "align to the
latest" decision is derived from history. It never initializes a repo or commits on your
behalf.
Usage
The basic unit is syncmd sync <path>, where <path> is a file or directory inside a git
repo:
syncmd sync . # sync every known group in the repo
syncmd sync CLAUDE.md # sync just the group CLAUDE.md belongs to
syncmd sync .claude/skills/foo/ # sync the `foo` skill across all roots
Because the unit is a path, it composes upward — point it at a project, or (later) a whole home directory of projects, and each repo is handled independently.
What "latest" means
For each member syncmd computes a recency:
- Uncommitted / untracked edits rank as now — your live edit to
CLAUDE.mdbeats a week-old committedAGENTS.md. - Otherwise, the committer date of the last commit that touched the file.
- A file that doesn't exist yet is only ever a creation target.
It finds the baseline — the most recent commit where the group last agreed — and looks at what changed since:
- One member changed → unambiguous, propagate it.
- Several changed independently → a real conflict; resolved per
--strategy, and losers are always backed up and reported.
Common commands
# Preview only — compute the plan, write nothing
syncmd plan .
syncmd sync . --dry-run
# Choose how divergence is resolved
syncmd sync . --strategy newest # default: newest edit wins (losers backed up)
syncmd sync . --strategy error # refuse to write, report the conflict, exit 1
syncmd sync . --strategy interactive # show diffs, you pick the winner
# Machine-readable output
syncmd sync . --json
# Safety toggles
syncmd sync . --no-backup # don't write <member>.syncmd.bak
syncmd sync . --allow-delete # propagate a deletion across the group
Worked examples
| Situation | Result |
|---|---|
A. Live edit — you edit CLAUDE.md (uncommitted), AGENTS.md already synced |
CLAUDE.md wins; AGENTS.md rewritten; re-run is a no-op |
B. Bootstrap — only CLAUDE.md exists |
the other members are created from it |
C. Divergence — AGENTS.md and CLAUDE.md edited in separate commits |
newest wins & backs up the other; error writes nothing and exits 1 |
| D. Converged — all members byte-identical | no-op, exit 0 |
E. Skill — foo exists only under .claude/ |
created under .agents/ and .github/ |
Exit codes
| Code | Meaning |
|---|---|
0 |
All groups in sync or successfully propagated |
1 |
At least one unresolved conflict (--strategy error/interactive abort) |
2 |
Precondition failure (not a git repo, bad path, bad config) |
3 |
I/O or git error |
Configuration
syncmd ships sensible built-in defaults (instructions, skills, agents, specs) — zero config
needed. To extend or override, drop a syncmd.toml at the repo root:
# A fixed list of equivalent files (order = canonical preference, used only to break ties)
[[rule]]
group = "instructions"
type = "claude_md"
layout = "file"
strategy = "newest"
mounts = [
{ harness = "agents", pattern = "AGENTS.md" },
{ harness = "claude", pattern = "CLAUDE.md" },
{ harness = "copilot", pattern = ".github/copilot-instructions.md" },
{ harness = "cursor", pattern = ".cursorrules" },
{ harness = "gemini", pattern = "GEMINI.md" },
]
# A templated, folder-backed group — each {name} becomes its own asset
[[rule]]
group = "skill:{name}"
type = "skill"
layout = "folder"
strategy = "newest"
mounts = [
{ harness = "claude", pattern = ".claude/skills/{name}/SKILL.md" },
{ harness = "agents", pattern = ".agents/skills/{name}/SKILL.md" },
{ harness = "github", pattern = ".github/skills/{name}/SKILL.md" },
]
SDK (Python)
The Rust core is exposed as a Python module. sync() and plan() return a report object
whose to_dict() matches the flowpad-sdk record shape (type/id first, snake_case, nulls
omitted), so it drops straight into flow-sdk tooling.
import json
import syncmd
# Preview — no writes
report = syncmd.plan(".")
for group in report.groups:
print(group.name, group.decision, group.winner_oid)
# Apply, choosing a strategy
report = syncmd.sync(".", strategy="newest", backup=True)
print(report.summary.written, "files updated")
# flow-sdk-shaped dict / JSON
print(json.dumps(report.to_dict(), indent=2))
print(report.to_json())
# Round-trips into flow-sdk records
from flow_sdk.fs_store import FSRecord
rec = FSRecord.from_dict(report.to_dict())
plan(path, *, recursive=False) and
sync(path, *, strategy="newest", dry_run=False, backup=True, create_missing=True, allow_delete=False, recursive=False) mirror the CLI exactly — the CLI and the SDK are both
thin wrappers over the same core functions.
How it works (one paragraph)
The blob object id (OID) is the only content primitive: two files are "in sync" when they
point at the same blob. Equality, "changed since the baseline", and group agreement are all
OID comparisons — syncmd never diffs bytes until it writes the single winning file. The
decision is made once per group (who wins); the actions are applied per member (write / create
/ skip / back up). All writes are atomic (temp file + rename) and overwritten files are backed
up to <member>.syncmd.bak first.
See docs/sync.md for the full algorithm specification.
See docs/releasing.md for the multi-registry release flow.
Status
v1 in development. In scope: markdown instruction/skill/agent/spec groups, git-history-based
recency, propagation, divergence detection. Out of scope (for now): non-markdown/binary
assets, the adapter-compiler layer (rendering one canonical source into harness-specific
manifests like MCP config.toml), networked/multi-repo sync, and auto-commit.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distributions
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file syncmd-0.1.0.tar.gz.
File metadata
- Download URL: syncmd-0.1.0.tar.gz
- Upload date:
- Size: 52.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
4e9a5e572fa3dcebaea9eae2113d91bc85a0f66a6e3b386be66fe5000859126a
|
|
| MD5 |
e80a3b08725e532be1985065f6d7232d
|
|
| BLAKE2b-256 |
8e7603d182ac770bac1aa8e1cd54a44e47366c9d8960fec7a858aef27a23aaea
|
File details
Details for the file syncmd-0.1.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.
File metadata
- Download URL: syncmd-0.1.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
- Upload date:
- Size: 2.0 MB
- Tags: CPython 3.9+, manylinux: glibc 2.17+ x86-64
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
5b42ca3bfee12fb4d4d04182225268fa1f7ecfc21bb423ac8e10c06338e58fac
|
|
| MD5 |
ec276c18c4f5c68b661cc3614eca8386
|
|
| BLAKE2b-256 |
bc8aa5a071dcbce32a4cf4dee670c0d0a2de9a8dd11b88dbba99263351e0b766
|
File details
Details for the file syncmd-0.1.0-cp39-abi3-macosx_11_0_arm64.whl.
File metadata
- Download URL: syncmd-0.1.0-cp39-abi3-macosx_11_0_arm64.whl
- Upload date:
- Size: 1.8 MB
- Tags: CPython 3.9+, macOS 11.0+ ARM64
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ab58afde4deb40fb001c3d607ea177d02cc97c5935113d3ecbd3ad44bdc7f362
|
|
| MD5 |
cbb1ac6a4691b459c7813678f20d8b4f
|
|
| BLAKE2b-256 |
d41a00e24ce4f466431e58a1da452a8c9d09316385548cce22c636984a128ca5
|