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[].pathis 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 incontent_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.mdbreaks every skill that carries references, scripts or data — which is most non-trivial skills. enabled: falsesuppresses 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.