Harnesses
Unified Harness Protocol, version 2026-10-19
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": [],
"plugins": [],
"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 |
plugins |
array | no | Installed plugins, each a package of tools and skills; see Plugins |
environment |
string | no | The environment this harness's tasks read, mounted read-only; requires the environments capability. See Environments |
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 |
The memories a harness's agent reaches are not a field of this object. The agent is a member and
is granted on the memory, like a person; /v1/harnesses/{id}/memories reads what it was granted
and keeps two settings (Memories §9).
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 |
This list carries remote servers only, exactly as it did in the previous version. An MCP server that
runs as a process (stdio) is declared inside a plugin, which is what gives it a root to run from
and a directory to keep state in (Plugins §2.2).
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.
4.4 Plugins
{ "plugins": [ { "name": "contract-review", "enabled": true, "blob": "plg_…" } ] }
A plugin is an Agent Plugins package: plugin.json,
mcp.json and skills/, carried as one folder and installed whole. Its servers and skills join the
harness's own for every turn; the harness's own mcpServers and skills report only what was
written to them and never absorb a plugin's. The object, the composition rule, the export of a
harness as a package, and what a hosted server owes a running plugin are in the
Plugins chapter. The capability is plugins; a server without it is conformant at
every class.
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".
A server MUST refuse, rather than store as nothing, what it cannot keep:
- a field the harness object does not have:
422, naming the field and, when one is close, the field it was probably meant to be; - a
default_modelits base does not run:422withcode: "model_not_available".
What the harness object comes back with and cannot be written (id, object, createdAt and the
server's own read-only fields) MUST be accepted and ignored, so that a harness read and sent back
saves. Both refusals hold for an update as well.
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}
Deleting a harness retires it. What happens to the sessions and responses that used it is the server's to choose, between two rules, and the server MUST say which one it follows:
Sessions are kept (the default). A server that reports the harness_retirement capability
false MUST NOT delete the sessions or responses that used the harness. They stay readable and
deletable on their own. History that disappears when configuration changes cannot be audited, so
this is what a client may assume of a server that says nothing.
Sessions are retired with the harness, under a restore window. A server that reports
harness_retirement true MAY remove the harness's sessions and responses with it, and only under
these rules:
- The window is stated when the harness is retired. The delete response MUST carry
sessions: "retired"andrestorableUntil, the time (Unix seconds) until which a restore is possible. A server's own interface MUST tell the person the length of the window before they confirm, and a client that retires a harness for a person MUST passrestorableUntilon to them. - Within the window, a restore brings both back. Restoring the harness (however the server
offers it) MUST bring back its sessions and responses with it, as they were. A server MAY
withhold them from lists and reads until then; a read of one MAY answer
404. - After the window, removal is permanent. The server MAY then remove the harness, its sessions and its responses for good. It MUST NOT remove them earlier, with one exception: a person's own request to erase their data MAY remove the retired sessions that are theirs before the window ends.
- Never mid-turn. A harness with a task in progress is not retired while it runs. The server
either cancels the task first, as
POST /v1/sessions/{session_id}/cancelwould, and then retires the harness, or refuses the delete with409andcode: "harness_busy", saying why.
A server MUST NOT remove a harness's sessions or responses when the harness is retired without a
window it stated. A server that keeps them MAY say so with sessions: "kept" in the delete
response; an absent sessions means kept.
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.