Harness Packs

August 15, 2026 · View on GitHub

A harness is an agent tool that can consume SkillRoute — Claude Code, Codex, Pi, Hermes, OpenCode, and the rest. Each one is described by a single declarative manifest in harnesses/, so adding support for a new tool is usually a data change with no Python at all.

skillroute harness list                  # everything SkillRoute knows about
skillroute harness list --mode skills    # only harnesses with a skills directory
skillroute harness detect                # what is actually installed here
skillroute harness show pi               # what setup would look like
skillroute harness install pi --dry-run  # print, change nothing
skillroute harness install pi --yes      # apply it
skillroute harness doctor pi             # prove the pack still works

Supported harnesses

first-party harnesses support every mode that makes sense for them and are exercised end to end. breadth harnesses are MCP-only.

idNameTierModesConfig shape
claude-codeClaude Codefirst-partyhook · mcp · router_skill · skillsmcp_servers
codexCodexfirst-partymcp · router_skill · skillscodex_toml
piPifirst-partyextension · mcp · router_skill · skillsmcp_servers
hermesHermes Agentfirst-partyacp · mcp · router_skill · skillsyaml_map
opencodeOpenCodefirst-partymcp · router_skill · skillsopencode_mcp
ampAmpbreadthmcpamp_mcp_servers
claude-desktopClaude Desktopbreadthmcpmcp_servers
cursorCursorbreadthmcpmcp_servers
deepseekDeepSeek Harnessbreadthmcpdsh_cordis_patch
gemini-cliGemini CLIbreadthmcpmcp_servers
gooseGoosebreadthmcpyaml_map
ibm-bobIBM Bobbreadthmcpmcp_servers
vscodeVS Codebreadthmcpvscode_servers
windsurfWindsurfbreadthmcpmcp_servers
zedZedbreadthacp · mcpzed_context_servers

The manifests are the source of truth; skillroute harness list --json always reflects what is actually installed.

Install modes

A harness declares which of these it supports. mcp is the baseline.

ModeWhat it does
mcpRegister the SkillRoute MCP server
acpAttach SkillRoute as an ACP routing-advisor agent
skillsRead the harness's native skills directory, and optionally project a routed subset back
hookInstall a lifecycle hook (Claude Code's SessionStart)
extensionInstall a harness-native extension package (Pi)
router_skillDrop in a generated SKILL.md that teaches the agent to ask SkillRoute first

Adding a harness

Most of the time this is one file.

1. Write harnesses/<id>.toml. The filename stem must match id.

schema = 1
id = "my-harness"
display_name = "My Harness"
tier = "unverified"          # first-party | breadth | unverified
homepage = "https://example.com/docs"

[detect]
commands = ["my-harness"]                  # looked up on PATH
client_names = ["my-harness"]              # MCP initialize clientInfo.name
paths.all = ["~/.my-harness/config.json"]  # or paths.macos / .linux / .windows
apps.macos = ["/Applications/My Harness.app"]

[modes.mcp]
setup_method = "json_merge"                # command | json_merge | print_only | dir_sync | package
emitter = "mcp_servers"                    # a named config shape (see below)
config_format = "json"
config_path = "~/.my-harness/config.json"  # shown to the user
write_path.all = "~/.my-harness/config.json"

2. Run the conformance suite. It is parametrized over whatever is in harnesses/, so your new file is picked up automatically:

uv run --extra dev pytest tests/test_harnesses.py -v

That checks the manifest validates, every placeholder resolves, the mode renders on all three platforms, detection finds it from its own declared paths, and it does not try to merge a format SkillRoute cannot write.

3. Verify against the real tool.

skillroute harness show my-harness       # eyeball the snippet
skillroute harness install my-harness    # then confirm the tool sees it

Once confirmed against the tool's own docs, promote tier off unverified.

Placeholders

Any string value may contain these; anything else is a validation error.

{harness_id} {server_name} {catalog} {backend} {repo_root} {scope} {server_json}, plus {server_argv}, which expands to multiple argv entries and is therefore only valid as a whole array element.

Config shapes (emitters)

A manifest picks a shape by name. Six of the fifteen shipped harnesses reuse mcp_servers unchanged — if yours matches an existing shape, you write no code.

EmitterShape
mcp_servers{"mcpServers": {name: {...}}} — the de facto standard
vscode_serverstop-level servers, name embedded in the object
opencode_mcpnested under mcp with an explicit type
zed_context_serverscontext_servers
amp_mcp_serversamp.mcpServers
codex_tomla TOML snippet
yaml_mapa YAML map under a configurable key
dsh_cordis_patcha DeepSeek Harness cordis.patch.yml MCP insert
claude_session_start_hooka Claude Code hook entry

Extra literal keys go in [modes.<mode>.extra] and are folded into the emitted server object — that is how IBM Bob gets cwd/disabled and Codex gets its timeouts, without either needing its own emitter.

If your harness genuinely needs a new shape, add one function to EMITTERS in src/skillroute/harness_render.py and a test pinning it. That is a deliberate, reviewable addition rather than routine work.

Two rules worth knowing

SkillRoute never rewrites your TOML or YAML. tomllib cannot write, there is no YAML dependency, and hand-merging someone's config file is not worth the blast radius. Harnesses configured in those formats use setup_method = "command" (drive their own CLI) or "print_only" (print a snippet to paste). A conformance test enforces this.

Prefer registering a directory over copying files into one. Hermes exposes external_dirs and Pi declares skill paths in its settings, so [modes.skills] register_in points SkillRoute's directory at them instead of duplicating bundles. No duplication, no sync drift.

Cross-platform paths

Every path table accepts all, macos, linux, and windows keys, and the most specific match wins:

write_path.macos   = "~/.config/goose/config.yaml"
write_path.linux   = "~/.config/goose/config.yaml"
write_path.windows = "%APPDATA%/Block/goose/config/config.yaml"

Render for another platform without being on it:

skillroute harness show goose --platform windows --json

Verifying a pack: harness doctor

Manifests encode config paths for fifteen tools that each move on their own schedule, and a stale path fails quietly — harness install reports success while writing to a file the tool no longer reads. doctor is how that stays honest.

skillroute harness doctor                  # every pack
skillroute harness doctor claude-code pi   # just these
skillroute harness doctor --no-probe       # static checks only, no subprocess
skillroute harness doctor --json           # for CI

Each pack gets six kinds of check:

CheckFails when
manifestthe pack declares no install modes (unverified tier warns)
platforma file-writing mode has no path for the current platform
detectnever — an absent tool warns, since you can doctor a pack you do not use
render:<mode>a mode no longer renders, e.g. an unresolvable placeholder
configthe config exists but cannot be parsed
serverthe configured server command does not answer an MCP initialize

The server check is the one that cannot be faked by inspection: it runs the exact command the config names, sends initialize, and waits for a JSON-RPC reply. Everything else proves the pack is describable; this proves it works.

Absent and unconfigured harnesses warn rather than fail, so the command exits non-zero only on real breakage and is usable as a CI gate:

skillroute harness doctor --no-probe --json > packs.json || echo "a pack is broken"

Run it on Linux and Windows too — platform is what catches a pack that was written against macOS paths only, which is exactly how v0.1 detection went wrong.

Migrating from skillroute mcp config

skillroute mcp config --client <id> still works and emits identical output, but it is deprecated and will be removed in 0.3. The deprecation notice goes to stderr, so --json stdout stays machine-parseable.

BeforeNow
skillroute mcp config --client codexskillroute harness show codex
skillroute mcp config --client claude-code --scope projectskillroute harness show claude-code --scope project
skillroute harness install codex

The clientharness rename runs through the Python API too: skillroute.client_setup re-exports the old names from skillroute.harness_setup for one release.