One canonical source of truth for skills, agents, rules, MCP servers, LLM endpoints, and global instructions across every LLM harness on your machine.
Install ·
First run ·
Architecture ·
Harnesses ·
Commands ·
Design
The problem
You use claude-code, crush, kilo, opencode, goose, cagent, zed — pick any three.
Every one of them keeps its own copy of the things you'd want shared:
- the same skill markdown, duplicated seven ways
- the same agent definitions, rewritten per harness
- the same MCP server registry, with seven different key names (
mcpServers, mcp, context_servers, extensions, …)
- the same LLM gateway URL and token, hand-copied into seven config files
Edit once. Forget the others. They drift. Things stop working in odd places.
The fix
┌──────────────────────────────────┐
│ ~/.config/harness-sync/ (git) │
│ │
│ • profiles/<name>.yaml │
│ • skills/<name>/SKILL.md │
│ • agents/<name>.md │
│ • rules/<name>.md │
│ • mcp.yaml │
│ • instructions/global.md │
└──────────────┬───────────────────┘
│ harness-sync apply
┌─────────────┴─────────────┐
▼ ▼
┌───────────────────┐ ┌──────────────────┐
│ per-harness │ │ 3-way merge │
│ adapter.Render() │ ◄──► │ via git │
└─────────┬─────────┘ │ merge-file │
│ └──────────────────┘
▼
~/.claude/ ~/.config/crush/ ~/.config/kilo/
~/.config/opencode/ ~/.config/goose/ ~/.config/cagent/
~/.config/zed/
One canonical tree → seven harness-native configs, each with the correct keys.
Install
One-line installer (recommended):
curl -fsSL https://raw.githubusercontent.com/lukaszraczylo/harness-sync/main/install.sh | bash
Picks the right release artefact for your OS/arch, verifies the SHA-256, drops the binary in ~/.local/bin (override with INSTALL_DIR=/usr/local/bin).
Homebrew (macOS / Linux):
brew install lukaszraczylo/taps/harness-sync
From source:
git clone https://github.com/lukaszraczylo/harness-sync
cd harness-sync
make build && make install # installs to ~/.local/bin/harness-sync
Requires Go 1.24+ and git on $PATH.
Self-update: once installed, harness-sync update pulls the latest release into the same directory as the running binary (--dry-run to preview).
First run
harness-sync detect # what's installed?
harness-sync init # pick which to import from; canonical tree created
harness-sync apply # propagate canonical to every detected harness
Real output on a machine with all seven harnesses installed:
$ harness-sync detect
claude-code detected
crush detected
kilo detected
opencode detected
goose detected
cagent detected
zed detected
init opens an interactive multi-select of detected harnesses
(use --no-prompt to take all, or --from claude-code,crush to be explicit).
On first apply, harness-sync writes a render snapshot under state/<harness>/
and commits the canonical tree — every subsequent apply is a git merge-file
three-way merge against that snapshot.
Supported harnesses
Each row was grounded against the real config file on disk and the
harness's official docs before the adapter was written. Wrong key names
silently produce configs the harness can't parse — that's a bug class
harness-sync goes out of its way to avoid.
| Harness |
Native config |
MCP key |
Provider / model config |
Skills path |
| claude-code |
~/.claude.json (merged) + ~/.claude/mcp_servers.json |
mcpServers (type: stdio/http) |
own subscription — not managed |
~/.claude/skills/ |
| crush |
~/.config/crush/crush.json (merged) |
mcp (type: stdio/http/sse) |
providers map + default_model |
~/.config/crush/skills/ |
| kilo |
~/.config/kilo/kilo.json (merged) |
mcp (type: local/remote) |
provider map + model + small_model |
~/.kilo/skills/ |
| opencode |
~/.config/opencode/opencode.jsonc (merged) |
mcp (type: local/remote) |
provider map + model |
~/.config/opencode/skills/ |
| pi |
~/.pi/agent/settings.json (merged) + ~/.pi/agent/models.json |
— |
defaultProvider + defaultModel + models.json provider |
~/.pi/agent/skills/ |
| goose |
~/.config/goose/config.yaml (merged) + custom_providers/<name>.json |
extensions map (type: stdio) |
GOOSE_PROVIDER + GOOSE_MODEL |
~/.agents/skills/ |
| cagent |
~/.config/cagent/default.yaml (starter) |
mcps |
providers map + per-agent model: |
— |
| zed |
~/.config/zed/settings.json (merged) |
context_servers |
language_models.openai.{api_url, available_models} + agent.default_model |
— |
Capability matrix
What harness-sync writes per harness. Harnesses with a built-in subscription receive MCP + skills + agents + rules + instructions only — provider/model/endpoint config is skipped to avoid conflicting with the harness's own auth.
Skills paths are grounded in each harness's official documentation (kilo.ai, opencode.ai, goose-docs.ai). kilo, opencode, and goose also scan ~/.claude/skills/ as a compatibility path, so the claude-code symlink passively reaches them already.
| Harness |
Built-in sub |
Providers |
Models |
MCP |
Skills |
Agents |
Rules |
Instructions |
| claude-code |
✓ |
— |
— |
✓ |
✓ |
✓ |
✓ⁿ |
✓ |
| crush |
— |
✓ |
✓ |
✓ |
✓ |
— |
✓ᶠ |
✓ᶠ |
| kilo |
— |
✓ |
✓ |
✓ |
✓ |
✓ |
✓ᶠ |
✓ᶠ |
| opencode |
— |
✓ |
✓ |
✓ |
✓ |
✓ |
✓ᶠ |
✓ |
| pi |
— |
✓ |
✓ |
— |
✓ |
— |
✓ᶠ |
✓ᶠ |
| goose |
— |
✓ |
✓ |
✓ |
✓ |
— |
✓ᶠ |
✓ᶠ |
| cagent |
— |
✓ |
✓ |
✓ |
— |
— |
✓ᶠ |
✓ |
| zed |
— |
✓ |
✓ |
— |
— |
— |
✓ᶠ |
✓ᶠ |
The matrix is machine-readable via adapter.Capabilities() — every adapter implements HarnessCapabilities (ManagesProviders/Models/MCP/Skills/Agents/Rules/Instructions) so tooling can inspect what will change before running apply.
Agents & rules — native where possible, folded where necessary
Each kind is delivered the way the target harness actually loads it (grounded in each harness's docs and config source — a wrong path is a silent no-op):
- Agents are markdown subagents. They map cleanly to claude-code (
~/.claude/agents/), kilo (~/.config/kilo/agents/ — plural; the older singular agent/ is a no-op on current Kilo CLI), and opencode (~/.config/opencode/agents/), all as directory symlinks. goose (YAML recipes) and cagent (inline YAML agents:) use incompatible agent formats, and crush/zed have no markdown-subagent concept — so agents are not propagated there.
- Rules (
rules/<name>.md) are topic-scoped instruction fragments. claude-code auto-loads a rules directory natively (ⁿ), so they're delivered as a ~/.claude/rules symlink — preserving each rule's optional path-scoping frontmatter. No other harness has a rules directory, so their bodies are folded (ᶠ) into that harness's global always-on instructions file — AGENTS.md (opencode/kilo/zed/crush), .goosehints (goose), or the per-agent instruction: (cagent). crush has no global instructions file, so harness-sync writes ~/.config/crush/AGENTS.md and registers its absolute path in options.context_paths (crush always re-adds its built-in defaults at load, so nothing is clobbered).
ⁿ delivered as a native rules directory ᶠ folded into the harness's global instructions file
Merged, not replaced. For harnesses with user-managed config keys (every one
except cagent), harness-sync reads the existing file, overlays only the keys
it owns, and writes it back. Your hooks, permissions, language servers,
editor settings — everything else — survive untouched.
Add a new harness: drop a Go package under internal/adapters/<name>/
implementing adapter.Adapter, register one line in cmd/harness-sync/main.go.
Commands
harness-sync detect list adapters + detection status
harness-sync show [harness...] print files each adapter manages
--all include not-detected harnesses
harness-sync init import from detected harnesses
--from a,b pick adapters, skip prompt
--no-prompt take all detected
--force allow re-init over existing canonical
harness-sync apply [harness...] render + write
--dry-run show plan, write nothing
--force overwrite without 3-way merge
--yes skip first-run confirmation prompt
--allow-incomplete apply with an unconfigured gateway
harness-sync diff [harness...] apply --dry-run shorthand
harness-sync profile list list canonical profiles
harness-sync profile use <name> switch active profile
--apply reapply automatically after switching
harness-sync rollback [n] git revert last N apply commits
harness-sync adapter list print registered adapters
harness-sync update reinstall the latest release in place
--dry-run print the install command instead
--install-dir DIR override target directory
Every command accepts --root <path> to point at a non-default canonical tree.
Production safety
- First-run prompt. The first
apply against detected harnesses asks for confirmation before moving existing files to backups and replacing them with symlinks. Use --yes (or run non-interactively in CI) to skip.
- Profile completeness.
apply refuses to proceed when the active profile's gateway.url or gateway.default_model is empty. The placeholder profile written by init is left blank on purpose — edit profiles/imported.yaml before applying. --allow-incomplete lifts the guard for tests.
- Init guard.
init refuses to overwrite an already-initialised canonical tree. Use --force to merge new harness imports into existing skills/agents/MCP without clobbering edits.
- Env-var substitution.
${VAR} references in profiles and the MCP registry are resolved against the process environment at apply time. A missing variable aborts the run with an explicit error — secrets never silently fall back to empty.
Profiles
A profile bundles the LLM stack — gateway URL, dummy token, model allowlist,
upstream provider keys. Switching profiles re-renders every harness in one
command.
# ~/.config/harness-sync/profiles/home.yaml
name: home
gateway:
url: https://gateway.lan
token: dummy-local-token # plaintext OK: gateway accepts any non-empty token
default_model: claude-sonnet-4-6
upstreams:
- name: anthropic
api_key: ${ANTHROPIC_API_KEY} # env-var substitution at render time
- name: openai
api_key: ${OPENAI_API_KEY}
- name: ollama
base_url: http://10.0.1.21:11434
models:
- id: claude-sonnet-4-6
alias: sonnet
- id: claude-opus-4-7
alias: opus
Dummy gateway tokens may be plaintext (they have no value if leaked).
Real provider keys must be ${VAR} references — harness-sync substitutes
at render time so the canonical tree never contains plaintext secrets.
harness-sync profile use work # switch the active profile…
harness-sync apply # …and re-render every harness
Conflict resolution via git
Every apply is a git-style three-way merge:
base ← state/<harness>/<path> (last render harness-sync wrote)
ours ← rendered_new (what harness-sync would write now)
theirs ← target file on disk (what's actually there)
| Situation |
Outcome |
target == ours |
skip (already in sync) |
target == base |
fast-forward write |
| disjoint changes |
clean merge, write merged content |
| overlapping changes |
write <file>.rej next to target, leave target alone, exit non-zero |
Resolve a .rej: open it, copy the bits you want into the target, delete the
.rej, run apply again. No bespoke conflict format — it's <<<<<<< markers
from git merge-file.
Roll back the last N applies: harness-sync rollback 1 calls git revert on
the canonical repo, then re-renders.
Architecture
A single Go binary, ~5000 LOC across 18 packages, 93 tests including an
end-to-end test that runs the real binary against a fake $HOME.
cmd/harness-sync/main.go # cobra entrypoint + adapter registration
internal/canonical/ # Bundle, Profile, MCPRegistry, Skill, Agent, Rule types + loader
internal/adapter/ # Adapter interface, Registry, FileSet
internal/adapter/common/ # shared building blocks (BuildProviders, BuildMCPMapStyled, MergeJSONKeys, MergeYAMLKeys, …)
internal/adapters/<harness>/ # one package per harness, ~50-100 LOC of harness-specific glue
internal/apply/ # render → 3-way merge → write pipeline + state snapshots
internal/merge/ # git merge-file wrapper
internal/gitx/ # thin shell wrapper over the git CLI
internal/render/ # deterministic JSON / YAML / TOML marshallers
internal/secrets/ # ${VAR} substitution with strict missing-key error
internal/cli/ # cobra subcommands (detect, show, init, apply, diff, profile, rollback, adapter)
internal/ui/ # huh-backed multi-select with non-interactive override for tests
tests/e2e/ # binary-level integration test
DRY: the four pre-existing claude-code-like harnesses share BuildProviders,
ProvidersAsMap, BuildMCPMapStyled, MergeJSONKeys, ImportMarkdownTree,
ParseFrontmatter, and StripJSONComments from internal/adapter/common/.
The four MCP dialects (claude / crush / opencode / zed) sit in one switch,
so the right type: discriminator goes to the right harness.
Design
Non-goals (v1): watcher daemon, GUI, remote sync (push the canonical git repo
yourself), keychain integration beyond env-var substitution.
If harness-sync saves you time, consider sponsoring continued work:
License
MIT — see LICENSE.