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/.yaml │
│ • skills//SKILL.md │
│ • agents/.md │
│ • rules/.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):**
```bash
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):**
```bash
brew install lukaszraczylo/taps/harness-sync
```
**From source:**
```bash
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
```bash
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:
```text
$ 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//`
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.
### Native config format
| 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/.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/.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//`
implementing `adapter.Adapter`, register one line in `cmd/harness-sync/main.go`.
---
## Commands
```text
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 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 ` 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.
```yaml
# ~/.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.
```bash
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// (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 `.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// # 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
- **Spec:** [`docs/superpowers/specs/2026-05-23-harness-sync-design.md`](docs/superpowers/specs/2026-05-23-harness-sync-design.md) — problem framing, canonical layout, adapter interface, merge semantics
- **Plan:** [`docs/superpowers/plans/2026-05-23-harness-sync.md`](docs/superpowers/plans/2026-05-23-harness-sync.md) — 26-task TDD implementation plan
Non-goals (v1): watcher daemon, GUI, remote sync (push the canonical git repo
yourself), keychain integration beyond env-var substitution.
---
## Sponsor
If harness-sync saves you time, consider sponsoring continued work:
- GitHub Sponsors → [@lukaszraczylo](https://github.com/sponsors/lukaszraczylo)
- One-off coffee → [monzo.me/lukaszraczylo](https://monzo.me/lukaszraczylo)
## License
MIT — see [LICENSE](LICENSE).