U Unified Harness Protocol 2026-08-11

Harnesses

Unified Harness Protocol, version 2026-08-11

Before a client can send work it needs to know what can run it, and with which models. This chapter defines discovery, the harness object, and — at conformance class Full — how harnesses are created and changed.

1. Discovering harnesses

GET /v1/harnesses
{
  "harnesses": [
    {
      "id": "chrn_08dae611630d467ab3e67ed792570ae5",
      "object": "harness",
      "name": "Research agent",
      "base": "claude-code",
      "baseLabel": "Claude Code",
      "defaultModel": "claude-sonnet-4.6",
      "systemPrompt": "",
      "mcpServers": [],
      "skills": [],
      "disabledTools": [],
      "maxStep": null,
      "timeoutSeconds": null,
      "createdAt": 1786403298205
    }
  ]
}

A server MUST return only harnesses within the caller's scope. The list MAY be empty — a server with no configured harnesses is valid, and a client MUST handle that rather than assuming index 0 exists.

GET /v1/harnesses/{harness_id}

Returns one harness object, or 404 with code: "harness_not_found".

2. The harness object

Field Type Required Meaning
id string yes chrn_-prefixed
object string yes Always harness
name string yes Human-readable label; not an identifier
base string yes Which harness runtime: codex, claude-code, hermes, …
baseLabel string no Display name for base
defaultModel string no Model used when a task omits model
systemPrompt string no Additional standing instructions
mcpServers array no MCP servers attached to this harness
skills array no Skills available to this harness
disabledTools string[] no Tools withheld from the agent
maxStep integer | null no Default step budget
timeoutSeconds integer | null no Default wall-clock budget
createdAt integer yes Unix milliseconds

base values are not enumerated by this specification. A server MAY support bases this document has never heard of, and a client MUST treat base as an opaque string — anything else means the protocol has to be revised every time a harness is released, which is exactly the coupling UHP exists to remove.

3. Models

GET /v1/models
{
  "backends": {
    "claude": {
      "default": "claude-sonnet-4.6",
      "models": [
        { "id": "claude-sonnet-4.6", "label": "claude-sonnet-4.6", "backend": "claude",
          "available": true, "default": true }
      ]
    }
  }
}
GET /v1/harnesses/{harness_id}/models
{
  "harness_id": "chrn_…",
  "backend": "claude",
  "default": "claude-sonnet-4.6",
  "fallback": "claude-sonnet-4.6",
  "models": [ { "id": "claude-opus-5", "available": true, "default": false } ]
}

3.1 available is a promise

available: true means the server can serve that model for that harness right now — a credential exists and the provider can reach it. available: false means a request for it will not run as asked.

A server MUST compute available, not assert it. Listing a model as available and then failing the task is the worst outcome for a client: it presents a choice to a user, the user picks it, and the work fails after they have committed to it.

A client SHOULD present unavailable models as disabled rather than hiding them, so a user can see that a model exists and is not configured, rather than wondering why it is missing.

4. Tools and skills

A configured harness carries three things that decide what its agent can do. All three are part of the harness object and are set the same way it is.

4.1 MCP servers

{ "mcpServers": [
    { "name": "vault", "url": "https://mcp.example.com/mcp", "transport": "http", "enabled": true }
] }
Field Required Meaning
name yes Identifies the server to the agent; sanitised to a CLI-safe identifier
url yes Endpoint
transport no http (Streamable HTTP, default) or sse
enabled no Absent or true means enabled
headers no Extra request headers
auth no Bearer token, or a server-side reference the server resolves

A server MUST connect only the enabled entries for a turn. A disabled entry MUST NOT be contacted at all — not connected and then hidden, which would still leak the turn's existence to whoever operates that endpoint.

An MCP server that cannot be reached MUST NOT fail the task. The turn MUST proceed without those tools, because a third-party endpoint being down is not a reason to lose the user's work. A server SHOULD make the degradation visible in the run rather than silent.

A server MUST NOT advertise MCP support it cannot deliver. If the harness runtime behind it cannot speak the configured transport, that is a broken installation, and reporting it only in a log file inside the workspace is indistinguishable — to the user — from the model refusing to use the tool.

4.2 Skills

A skill is a folder, not a file:

{ "skills": [
    { "name": "vault-manual", "enabled": true, "files": [
        { "path": "SKILL.md",            "content": "---\nname: vault-manual\n---\n…" },
        { "path": "references/codes.md", "content": "…" },
        { "path": "scripts/helper.sh",   "content": "#!/bin/sh\n…" },
        { "path": "assets/logo.png",     "content_b64": "iVBORw0KGgo…" }
    ]}
] }
  • files[].path is relative to the skill's own folder and MUST support nested directories. A server MUST reject a path that escapes the folder.
  • Text is carried in content; binary in content_b64. A server MUST preserve both byte-for-byte.
  • A bundle MUST contain a SKILL.md; a server MUST reject one that does not, at configuration time rather than at run time.
  • A server MUST materialise the whole folder where the agent can read it. Materialising only SKILL.md breaks every skill that carries references, scripts or data — which is most non-trivial skills.
  • enabled: false suppresses a skill, including one inherited from the base.

A server MAY store large bundles out of line, and MUST return the complete file list from:

GET /v1/harnesses/{harness_id}/skills/{skill_id}/files

Round-tripping a harness through GET and PUT MUST NOT lose skill contents. This is the failure worth designing against: an unrelated edit — renaming the harness — silently emptying a skill folder that the user cannot tell is gone until an agent behaves oddly weeks later.

4.3 Disabled tools

{ "disabledTools": ["WebSearch"] }

Names come from the harness base's own tool catalogue. Enforcement differs by runtime and a server MUST NOT overstate it:

  • Where the runtime supports per-tool restriction, the server MUST enforce it as a hard block.
  • Where it does not, the server MUST still convey the restriction to the agent — as a standing instruction — and MUST NOT silently drop it. Dropping it is the worst outcome: the operator believes a tool is off, and it is not.

A client that requires a guaranteed block SHOULD confirm the harness base supports one rather than assuming disabledTools is always hard.

5. Managing harnesses

Conformance class Full only. A client MUST check the harness_management capability first.

5.1 Create

POST /v1/harnesses
{ "name": "Research agent", "base": "claude-code", "default_model": "claude-sonnet-4.6" }

Returns the created harness object. base is REQUIRED and MUST be one the server supports; otherwise 422 with code: "unsupported_base".

5.2 Update

PUT /v1/harnesses/{harness_id}

Replaces the mutable configuration. A server MUST NOT change id, base, or createdAt. Changing the base of an existing harness would silently change the behaviour of every session already attached to it; a client that wants a different base MUST create a different harness.

5.3 Delete

DELETE /v1/harnesses/{harness_id}

A server MUST NOT delete the sessions or responses that used the harness. History that disappears when configuration changes cannot be audited.

6. Choosing a harness

Non-normative, but the question every client faces:

If the task is… Consider
Code editing in a repository A coding-specialised base (codex, claude-code)
Long-horizon multi-tool work A base with an explicit step budget and strong tool use
Cheap, high-volume classification The smallest model on any base

The point of UHP is that this choice stays reversible. A product that speaks UHP can change base or model with a configuration edit, and can A/B two harnesses against the same input without a second integration.