Skip to content

Canonical harness model — one definition, many control planes

Status: open/accepted

  • The agentic-harness surface is held as a single canonical, host-neutral model — capabilities, commands, hooks, MCP servers, permissions, and context documents — and exporters project it onto each host’s native shape. One definition drives Claude Code, Codex, Cursor, and Gemini; no target is privileged.
  • The model is the source of truth. An importer bootstraps it from the existing Claude surface so no capability is hand-transcribed, but once imported the model — not any host — is authoritative.
  • Markdown bodies validate with markdown-contract; the model is a plain JSON-serializable Zod shape that is itself the portable interchange form. Adding a host is a new exporter module behind a registry, never an op edit.

Accepted as the harness architecture, superseding this decision’s own first draft (a Claude-derived, skills-only model with Claude as the reference target). Realizes D-VSLI-distributed-work-runner-architecture’s “the Claude plugin ends as one surface among several.”

The harness is the skill-layer surface D-VSLI-distributed-work-runner-architecture installs into a host: today .claude-plugin/ manifest + skills/<name>/SKILL.md + conventions/*.md + .claude/settings*.json (hooks, permissions, MCP servers), wired in by harness install claude (D-0014-cli-primary-npm-distribution). That surface is authored ONCE, in Claude Code’s native shape, and there is no seam that lets the same capabilities reach a different host — a second host means a second hand-maintained copy of every capability, hook, and server. Different hosts (Codex, Cursor, Gemini) express the same concepts in different layouts and vocabulary; nothing models those concepts independently of Claude.

Adopt a canonical harness model and a set of equal-footing exporters over it.

plugin/lib/services/harness/model.ts — a plain, JSON-serializable Zod shape whose every field is host-neutral:

FieldIsRendered by each host as
metadataidentity (name, version, description, author, homepage)plugin/config manifest
capabilities[]invocable units: kind (skill | command), name, description, instructions, allowedTools, argumentHint, resources[]SKILL.md / slash command / custom prompt / rule
hooks[]lifecycle hooks: event, matcher, actionsettings hooks / config notify
mcpServers[]MCP servers: transport, command/url, args, env.mcp.json / config.toml / mcp.json
permissionstool allow/deny/ask rule setssettings permissions / approval policy
documents[]shared context docsconventions / AGENTS.md / GEMINI.md / rules

Capability and document bodies validate through a markdown-contract contract, so a malformed body fails at the model seam, not in a downstream host. The model IS the interchange format — the generic exporter emits it verbatim as harness.json.

plugin/lib/services/harness/import/ — reads an existing Claude surface (skills, conventions, and settings*.json/.mcp.json hooks, permissions, MCP servers) into the canonical model. This seeds the model faithfully without hand-transcription; it is a migration aid, not the ongoing source of truth.

plugin/lib/services/harness/exporters/ — one pure model → files module per target, registered by name behind a registry:

  • claude.claude-plugin/ + skills/ + commands/ + conventions/ + settings.json (hooks, permissions, mcpServers).
  • codexAGENTS.md roster + prompts/<name>.md + config.toml ([mcp_servers], approval).
  • cursor.cursor/rules/*.mdc + .cursor/commands/ + .cursor/mcp.json.
  • geminiGEMINI.md + .gemini/commands/*.toml + .gemini/settings.json (mcpServers).
  • json — the model verbatim, the host-independent interchange form.

Where a host lacks a concept (Codex has no hard tool-permission gate), the exporter renders the model’s field in that host’s idiom (informational guidance) rather than dropping it. sdlc harness export <target> runs an exporter; both it and sdlc harness model are ops on the deterministic substrate (D-0007-deterministic-op-substrate).

  • A single edit to a capability, hook, or server reaches every host on the next export — the drift trap closes across four control planes.
  • Claude loses its privileged position: no model field is Claude-specific, and the Claude exporter is peer to the others. harness install claude stays as the settings-registration path; a later slice may route it through the exporter.
  • The model grows by adding host-neutral fields, never host-specific ones. A host that needs a concept the model lacks motivates a new field every exporter can render.
  • Round-trip importers for non-Claude hosts (export-only for codex/cursor/gemini).
  • Re-authoring the corpus by hand against a native model manifest — the importer makes that a later, optional migration.

← Back to Decisions