HarnessRouter
The self-hosted, Apache-2.0 edition of HarnessRouter: it puts sixteen existing agent CLIs — Codex, Claude Code, Hermes, DeepSeek Harness and twelve more — behind one OpenAI Responses-compatible API, with sessions, streaming, files, cancellation and structured failures, and it carries the Unified Harness Protocol it implements together with the conformance suite that measures it.

What it is
HarnessRouter Community Edition is the open-source half of a two-part product: a self-hosted server that puts existing agent harnesses behind one interface, and the Unified Harness Protocol it implements. A single Docker container runs three processes — a Next.js console on the only published port, a gateway that speaks the Responses-compatible API, and a runner that starts the CLIs — with the gateway and runner on loopback and one /data volume holding the database, files, secrets and workspaces. Sessions are separated by operating-system user and workspace rather than by container, so a harness keeps a real shell, a real filesystem and real git. Sixteen bases are supported, each pinned to an exact upstream version and verified at install time: Codex, Claude Code, Hermes, Pi, DeepSeek Harness, OpenCode, Qwen Code, Gemini CLI, Cline, Oh My Pi, goose, Kimi Code CLI, Aider, OpenHands, CheetahClaws and System One. A product calls one endpoint, names the harness in metadata.harness_id, and gets persistent sessions, streaming progress, files and artifacts, cancellation and structured failures; a custom harness adds its own instructions, tools, MCP servers and skills without a redeploy. Environments — a project’s files and packages, built once and mounted read-only — became the protocol’s seventh object type on 2026-09-28. Apache-2.0, provider keys your own.
Who built itThe maintainer who answers in the pull requests. He holds 331 of the repository’s 603 commits under two identities — richard-epsilla with 310, Richard Song with 21 — and two addresses, richard@epsilla.com with 271 and richard@readily.ai with 21. A GitHub Actions bot accounts for another 209 commits, eighteen people are listed as contributors, and 477 of the 603 commits carry a co-author trailer.
How it is put together
The parts · 6A server that turns existing agent runtimes into backends behind one contract, with the contract, the implementation and the suite that measures it in the same repository. Three processes share one container: a console on the only published port, a gateway that answers the Responses-compatible surface and owns harness lifecycle, the model catalog, the control store and the plug planes, and a runner that starts a CLI per turn inside a session workspace. Sessions are separated by operating-system user and directory rather than by container, so each harness keeps a real shell, a real filesystem and real git, and the isolation work goes into file modes, group ownership and per-environment identities instead of images. The measurement apparatus is part of the architecture rather than an afterthought: the support matrix, the custom-harness dimension, the family tour, the plugin, browser and environments columns and the benchmark each read their verdict out of the turn record. Because a harness is registered in seventeen places that fail silently when missed, the honest reading of this design is that its hardest problem is not routing a turn but keeping a capability from disappearing without a sound.
- protocol/
- The standard itself: three dated versions across 36 files, a schema builder with OpenAPI and JSON Schema (27,956 bytes growing to 52,992), CONNECTING, SERVING, GOVERNANCE, VERSIONING and naming documents, a 16 KB changelog, and the conformance suite — 37 files, of which
uhp_conformance/checks.pyalone is 108,299 bytes, beside a CLI, a report writer, stubs and a fixture package.reports/already holds measured runs for harnessrouter-ce, for the hosted HarnessRouter and for a third-party server, superqode.protocol/site/build.py(69,851 bytes) builds the published site, which asserts a description on every page. - gateway/ and runner/
- The two server processes.
gateway/app.pyis one file of 1,025,053 bytes holding the Responses surface, harness lifecycle and the catalogs, besidemedia_plane.py(106,461),plugs_plane.py(47,280),browser_plane.py(39,196), the control and SQL stores, and 74 test files.runner/server.pyis 465,197 bytes with a driver per CLI next to it — aider 36,780, openhands 36,763, dsh 28,339, cheetahclaws 20,982, systemone 14,611 bytes — and 60 test files, several of which pin the relay and the normalisers rather than the behaviour they carry. - ui/
- A Next.js console, 121 files and 1.4 MB: routes for harnesses, environments, integrations, kits, keys, plugins and tasks, with
HarnessSettings.tsxat 51,445 bytes andTaskChat.tsxat 46,893;lib/harness.ts(47,865 bytes) holds the backend union type and the built-in catalog with the upstream version of each base. Three large stylesheets carry most of the surface —revamp.css127,436,hr.css100,961,v2.css79,554 bytes — and a studio directory holds the traces and workflow views. - docker/ and .github/workflows/
entrypoint.shis 59,305 bytes and installs each harness CLI on first start at an exact version it then verifies: where upstream publishes checksums it fetches and compares them, where it does not it computes the digests itself and pins them beside the version. Two more scripts install the starter kits and the skills, and two Dockerfiles cover the image and a reference runner. Eight workflows sit beside them: release.yml (13,554 bytes), tests.yml, conformance-measure.yml, conformance-remeasure.yml, open-pr.yml, readme-stars.yml and the cleanup job.- docs/ and scripts/
- The apparatus the claims rest on:
support-matrix.mdat 345,599 bytes with its results file at 3,033,683 and its notes at 128,496,self-hosting-guide.mdat 59,286,harness-verification.mdat 15,937,environments.mdat 11,866, and the benchmark page with its 213,287-byte results file. The scripts that produce those tables are in the tree too — the Playwright-driven matrix, the Python columns for environments, plugins and the browser, the custom-harness and family-tour drivers, the SpreadsheetBench benchmark pack, and a responsive audit that sweeps every console surface at every width. - The repository root
- README.md at 21,440 bytes, CONTRIBUTING.md at 3,714 with the rules for how a change lands on main, a 2,541-byte code of conduct, a 1,328-byte security policy, the 2,336-byte third-party notice and the Apache-2.0 licence.
docs/images/is the largest thing in the repository: 25 files and 20.9 MB of the 34.7 MB total, mostly GIFs and console screenshots used by the README and the support-matrix tables.
Choices, and what they beat
A task names its environment in
metadata.environmentover a top-levelenvironmentfield on the requestWritten into the changelog as a reversal: the task surface is a Responses request and
metadatais its extension point, whereharness_idalready travels, so a top-level field would be a second convention for the same idea and one no Responses SDK sends unchanged. The published chapter had carried the field at the top level for exactly one day before the specification, the schema, the suite and the reference server changed together.A configured harness is the addressable unit over letting a client select a bare base
The same base behaves very differently with a different system prompt, a different model or a different tool set. Making the configuration a first-class, addressable object means a product can change how its agent behaves without redeploying its backend, and means two products can share a server without sharing behaviour.
The session is implicit, created by the first task over requiring a session to be created before any work
Stated in the specification with its own arithmetic: requiring
POST /sessionsfirst would add a round trip, a failure mode and an object to clean up, for a concept most first tasks never need. A client that only ever sends one-shot tasks never learns the word session; a client that wants continuity quotes the id it already has.An environment is mounted read-only beside the session’s own workspace over treating the built environment as the artifact namespace
It is never part of a container: artifacts come from the working directory, and the environment is what the working directory reads. Isolation is drawn with operating-system groups and identities rather than containers — each environment gets a group derived from its id, its source is 0700, a built version 0750, the store traversable but not listable — because the installs run the packages’ own code and must not run it as root.
The pull request is opened by a bot identity over the operator opening it, or opening it with a personal access token
mainrequires one approval with no bypass for anyone, admins included, and a pull request cannot be approved by its own author — so an operator working alone, or through an agent using their account, could never merge. A personal access token would not do for the same reason. The workflow header also records that a bot-token pull request does not start the checks workflow at all, so the repository uses a GitHub App’s installation token instead.
Read fromprotocol/versions/2026-09-28/architecture.md, protocol/CHANGELOG.md, protocol/IMPLEMENTATIONS.md, protocol/versions/2026-09-28/harnesses.md, CONTRIBUTING.md, .github/workflows/open-pr.yml, docs/harness-verification.md, scripts/support-matrix/README.md, ui/src/lib/harness.ts, docs/benchmark.md, README.md, and the complete 508-file tree with its per-file and per-directory sizes.
Build log
6 stages- 01
Fifty-two days, and a release every few hours
The repository was created on 2026-08-09 at 20:19 UTC, and its first commit arrived half an hour later — “Self-contained core: gateway + runner running with no cloud”. From there to the newest commit on 2026-09-30 it collected 603 commits: 303 in the twenty-two days of August that remained, and 300 in September. The twenty most recent releases in the recon run from
v0.25.17on 2026-09-28 at 17:19 tov0.27.0-rc.2on 2026-09-30 at 10:12, and fourteen stable versions were published on 2026-09-29 alone:v0.26.0at 00:19, then 0.26.1, .2, .3, .5, .6, .7, .8, .9, .10, .11, .12 and .13 by 09:46, then 0.26.14 at 17:27, withv0.27.0-rc.1that evening. 2026-09-30 added 0.26.15, 0.26.16 and 0.26.17 either side ofv0.27.0-rc.2. The version moves one patch number per bugfix, andv0.26.4appears nowhere in the sample. Around all of it sit 2,793 stars, 285 forks, 21 watchers, 12 open issues, eighteen contributors, 508 files and 34.7 MB of repository, Apache-2.0 and Python. - 02
Four hundred and seventy-seven trailers, and a bot that opens the pull requests
The tail of every commit says who wrote it. 477 of the 603 carry a co-author trailer, and 433 of those name a Claude model: Claude Fable 5.1 on 195 commits, Claude Opus 5 (1M context) on 128, Claude Fable 5 on 81, Claude Opus 4.8 on 21, Opus 5 on four, Opus 5.5 (1M context) on two, and Opus 4.8 (1M context) with Sonnet 5 on one each. People appear as co-authors too — AIinWEB3 on fourteen commits, the maintainer on ten, sakurahello1, ZixiaoL and Hao Lu on three each. A further 209 commits are authored by
github-actions[bot], and that identity is deliberate.maintakes pull requests only: one approval, every check green, no bypass for anyone, admins included. A pull request cannot be approved by its own author, so an operator working alone, or through an agent using their account, could never merge at all.open-pr.ymlopens the pull request asgithub-actions[bot]instead, which makes the maintainer a reviewer rather than the author. Its header also records the failure that shaped it: a pull request opened withGITHUB_TOKENdoes not start the checks workflow, because GitHub guards against recursive runs, and one docs pull request sat blocked for exactly that reason on 2026-09-06. The repository now opens them with a GitHub App’s installation token; a personal access token would not do, the comment says, because the operator would then be the author. - 03
A community fix, re-measured against the binary, and taken another way
Pull request 332 came from lab1207 as the first slice of issue 202: models Codex does not know get no
apply_patchtool, and the model then loops on an unsupported call. The reply is the best record in the repository of how a change is settled here. The maintainer thanks the author for a careful reading ofspec_plan.rs, agrees the diagnosis is right, and then measures the claim against the Codex they ship (0.154.0) and finds the lever elsewhere:apply_patch_tool_typeis a field of Codex’s model catalog, not aconfig.tomlkey; the shipped binary accepts it at the top level and ignores it; no config reader consumes it; and its enum has one value,freeform. The catalog can be replaced throughmodel_catalog_json, but the tool is registered only inside Codex’s own multi-environment executors. A stub Responses endpoint that recorded the request showedgpt-5-codexand an unknown model sending identical tool lists, withoutapply_patch, either way. 332 was closed in favour of 346, which carries the original author’s co-authorship on the commit. A second community change, 326 from kuishou68, is smaller and just as telling: two npm scripts pointed at aui/doc-editor/scriptsdirectory that is not in the tree, sonpm runno longer offers commands that fail immediately. - 04
What it takes to prove a harness works
The project states its own standard in one sentence: a harness that answers is not a harness that works. For every harness and every model its menu offers, one session runs five scenarios — a first turn, a follow-up, a switch to another model and back, an artifact the task must produce, and a recycle in which the sandbox is let go on purpose and a follow-up must still recall the first message. Four rules turn a run into a verdict, each enforced in code rather than by eye. One compares every turn record’s own connection stamp against the connection under test. One compares the model asked for with the model served, and that rule used to be unenforceable on three harnesses — goose, cline and qwen report no served model — until every turn on those backends was routed through a loopback relay that reads
modeloff the provider’s own bytes as they pass. One requires the rendered file cards to be the turn’s stored files, because asking only whether some card carried the expected name let a file rendered twice pass as a produced artifact for months. The family tour answers a similar failure: on 2026-09-27 a person found in five turns that a CheetahClaws turn which answered in full had been recorded as a failure, because a smaller context made the CLI compact its history mid-turn and the driver judged the turn by a position in that history. Adding one harness means seventeen registration points, and every one of them except the console’s backend union type fails silently when missed. - 05
A standard published the same day it is changed
The protocol lives in the same repository and has three dated versions in seven weeks — 2026-08-11, 2026-09-12 and 2026-09-28 — with the JSON Schema growing from 27,956 to 52,992 bytes and the OpenAPI document from 40,505 to 76,548, and the conformance suite alongside them at 2026.9.12.post3, post4, 2026.9.28, then post1, post2 and post3 inside one day. The 2026-09-28 chapter first put a task’s environment at the top level of the request; review reversed it to
metadata.environment, and the changelog says so plainly — the published chapter had carried the field at the top level for one day — while the specification, the schema, the suite and reference server 0.26.7 changed together. The same day, EN-02 stopped failing on every server (83 of 84) by reading a field the client’s own result object never had, and the full class then passed 84 of 84. A security review found a 422 fromPUT /v1/admin/integrationsrepeating the request body with the provider key in clear, because FastAPI’s default validation response carries each failing value underinput; the answer is now a field-level envelope, and a test posts a fake key and asserts it is absent from the response. A site build that asserts a description on every page, meanwhile, kept the protocol site at its previous version until the new chapter was given one. - 06
Two kinds of tradeoff, both written down
The specification carries its reasons in the text rather than in a design document. Why a configured harness and not just a base: the same base behaves very differently with a different system prompt, model or tool set, so making the configuration addressable lets a product change how its agent behaves without redeploying, and lets two products share a server without sharing behaviour. Why the session is implicit: requiring
POST /sessionsfirst would add a round trip, a failure mode and an object to clean up, for a concept most first tasks never need. Absent is not empty, and empty is not zero. And a server must not advertise a capability it does not implement, because advertising is a promise. The measurement rules are the same kind of writing: one provider at a time, and a run owns the instance, since a deploy in the gap between a worker’s turns kills a session and costs the whole column. The code records a different bargain.gateway/app.pyis a single Python file of 1,025,053 bytes andrunner/server.py465,197 bytes, withgateway/tests/test_media_mcp.pyat 182,516 andtest_media_attack.pyat 93,274 — size accepted, apparently, in exchange for the harness registry living in one place. Even the automation has a dated story: the plugin matrix has been kept in the repository since 2026-09-18, after the copy in a scratch folder was emptied mid-review.
Adjacent records
All records →No. 070
OpenChatCut
A local-first video editor whose editing surface is a conversation: the built-in agent and external Codex or Claude Code sessions call the same editing tools the interface itself uses, so every change lands on a real multi-track timeline as a clip, transition, caption, effect or audio item that can still be dragged, undone and exported. Projects and media stay on the machine, and preview and final render both come out of Remotion.
No. 064
delegate-skills
A skills package in which every coding-agent CLI gets its own delegation skill: the orchestrating agent writes a self-contained brief, a separate CLI edits a real working tree, and the human keeps the review and the commit.
No. 061
Reticle
An MCP server and a dev-only SDK that let a coding agent read and drive a running web or desktop app from the inside, then answer with a verdict and the file and line to fix instead of a screenshot.