Skip to main content
A harness is written once per domain and read by every session of every campaign in it. The smallest one is two files; the trading example has thirty-six. Start small, run a build, read what the sessions did, and add what they lacked.

Start

The manifest names the harness and its version; the guardrails page holds the domain’s standing rules and where to look. That already loads on a build: every session gets the page under its Domain guidance heading, nothing else changes.

The directory

Everything is defined once, under its kind, and listed per module in the manifest. A file that exists and is listed nowhere is a warning; a name listed and not defined is a failure. Each attachment takes the cheapest form that does its job — the ladder: a line on a page before a skill, a skill before a delegate, a delegate before a hook. What only restates a page is a page line; a server that would return fixed text is a page too; a hook earns its place by acting on an event, not by reminding.

harness.yaml

name, version and description are required; the directory may be named anything, name is what the engine records. Every module has the same four lists, and each list is complete: nothing is inherited between modules, so a hook wanted in six modules is listed in six modules. A list left out is empty, and a module left out gets its pages and nothing else. A skill name is a directory under skills/, an agent name a file under agents/, a server name a key in mcp.json, a hook name a directory under hooks/.

Pages

instructions/guardrails.md reaches every session; instructions/<module>.md reaches that module’s sessions, after it. Both go into the prompt whole, under a Domain guidance heading that ends with the paths of the task statement and the prepared resources, so a page says “the task statement” or “the diff” and never a path it would have to guess. They are short and dense: standing rules, the discipline of the domain, where to look. A page over the config’s harness.page_warning_lines (40) — forty-one lines and up, every line of the file counted, blank lines and headings included — draws a warning from check, never a failure. A page never holds a value that belongs to one task: the task statement carries the brief, and its values win. A prepared resource is a directory the setup session leaves under prepared/<name>/, nothing is registered; the files an attempt saves under results/ are the harness’s own convention, the engine requires none. A module with a page or any attachment has content. The setup session and the verification sessions run only when their module has content; the other modules always run.

Skills

A skill is a directory under skills/ with a SKILL.md whose front matter has name (the directory’s name) and description (what it does, then Use when ...), and whatever scripts it needs under it (the example keeps them in a scripts/ directory). Those two keys are all check reads; the host reads the rest, and one other key the engine does read: a deployment target’s metadata.run_kinds, an optional list of the run kinds its workflow ends in, each one of function, http or script (the example’s paper-trading skill declares [script]); deploy refuses a result that names another kind, and a list that is empty or holds any other word is refused when the skill is read. The engine stages it where the host looks for skills, under the session’s own directory; the host reads the front matter and loads the body when the description matches the work.
KAPSO_HARNESS_ROOT is set in every staged session and names the harness directory, so a script is run where it lives. Name each skill on its module’s own page — the directory name, literally, anywhere in that page’s text is what check looks for; a module with no page of its own is read on guardrails.md, and only then does a mention there count — since it warns on one the page it reads never mentions: a session that is never told about a skill does not look for it.

Servers

mcp.json holds the tool servers, by name, in the hosts’ own format:
A stdio server has command, args, env and cwd; a streamable-http server has type, url and headers. A stdio server is any program that speaks MCP on its stdin and stdout; the example’s are Python on the official SDK’s FastMCP (from mcp.server.fastmcp import FastMCP, one @server.tool() per tool, server.run()), with mcp in requirements.txt. ${PLUGIN_ROOT} is the harness directory, expanded at staging. A server runs with the session’s directory as its working directory, with KAPSO_HARNESS_ROOT set, beside the engine’s own gates; on Codex the engine forwards the variables the server names, which that host does not inherit. Names match [A-Za-z0-9_-]+; gated-knowledge is the engine’s own server. Every env and headers value is one ${NAME} reference, the whole value (Bearer ... prefix included, so the variable holds the whole header), and an env entry names the variable it forwards ("BROKER_API_KEY": "${BROKER_API_KEY}", never another name: Codex forwards by name). The value lives in .env beside the run, or in the environment. A literal, a credential-shaped value, a reference embedded in a longer value or an env key that references another variable is a failure; an unset reference is reported as something the machine lacks, scoped to the modules that mount the server, and a build whose modules need it refuses before any session.

Delegates

A delegate is a file under agents/ with name and description in its front matter and its instructions in the body; those two keys are all the engine reads and stages (a tools or model key would be dropped), so a delegate works with the session’s tools and model. A session hands it one piece of work and reads its report; on Claude Code it is a sub-agent, on Codex a role, and on both it gets the module’s pages. The trading harness has two: an auditor the verifier asks about a suspect pattern, and a risk reviewer the judge asks about a drawdown.

Hooks

A hook is a directory under hooks/ with a hook.json in Claude Code’s hooks format and its script beside it. The engine merges every hook a module lists into one file per session, in manifest order, after its own protect-harness hook, and hands it to the host: --settings on Claude Code, .codex/hooks.json on Codex. Both hosts run the script with the event on stdin and read its decision from stdout. The engine’s hook denies a file-tool write under the harness directory with the reason: the harness is yours, read-only in every session, and a session that finds it wanting reports that for you rather than patching it. A write through the shell is caught at the next session, which refuses to start on a harness that no longer hashes as recorded and names the files that changed.
The events are SessionStart, SessionEnd, UserPromptSubmit, PreToolUse, PostToolUse, PermissionRequest, Stop, SubagentStart, SubagentStop, PreCompact and PostCompact. An entry is {"matcher": ..., "hooks": [...]} with the matcher optional on every event: a tool event without one fires for every tool, and SessionStart, Stop and the other session-wide events take none. check splits the command like a shell, skips leading NAME=value words (PYTHONPATH=${PLUGIN_ROOT} python3 ... is fine) and verifies that the first program word is on PATH or an existing path. A command names python3, not python: a stock Ubuntu has only the former. The host runs the script from the session directory, the same directory the servers get as cwd, so a relative prepared/ or results/ path resolves there, with KAPSO_HARNESS_ROOT set. What a script sees differs by host in three places: A file hook reads both shapes, as the trading harness’s protect-prepared and scan-writes do. Exit 0 with nothing printed is no opinion. A PreToolUse denial (permissionDecision and permissionDecisionReason) and a SessionStart or PostToolUse context line (the additionalContext key, as the trading example’s session-brief and scan-writes print) go inside hookSpecificOutput beside hookEventName; a Stop hook’s decision is top level, {"decision": "block", "reason": "..."}, and it sees stop_hook_active turn true after it blocked once, on both hosts, which is when it stays silent.

Environment

environment.python names a requirements file: one requirement per line in pip’s form (numpy, numpy==2.1.0, numpy>=2.0, an environment marker if needed; # comments, blank lines and -r-style option lines are skipped; a line that is not a requirement stops check with pip’s own parse error). check reads each against python3 on PATH, the interpreter the harness’s own commands name — a bare name needs the package installed, a specifier needs the installed version inside it — and reports what is missing or too old, with the python3 -m pip install line. environment.commands are programs that must be on PATH, environment.gpu asks for an NVIDIA GPU. A program a hook or a server runs is checked without being listed. Nothing is installed by the engine: a need is reported by check, printed by kapso harness list, and refused by a verb whose modules need it.

Check, run, read

check fails on what cannot load (an unknown module or field, a name that resolves to nothing, a skill or delegate without front matter (a page has none), a malformed mcp.json or hook.json, a hook with an unknown event, a script that does not exist, a credential literal, a missing requirements file), warns on what should be looked at (a file no module lists, a skill its module’s page never mentions, a long page, a long opaque value) and reports what the machine lacks. It runs no script and reads no path a SKILL.md quotes. The reference lists every rule. Then run a task on it and read the campaign: kapso_task.md, each session’s session.json and stream.jsonl, the verdicts. A hook that never fired, a skill no session used, a page a session contradicted, a verdict without evidence: each is a sentence to add or a file to cut. The trading example is that loop, with a task tuned so the shipped strategy has room to improve.

Version and share

Bump version when behaviour changes; the engine records name, version and a hash of the whole directory with every run, and a resume refuses content that is not the launch’s. A harness in a git repository is named by reference, acme/harnesses@v3#trading, fetched into the store with kapso harness add, and read from there by every build that names it.

Never in a harness

An evaluator, a dataset, a goal, a budget, a credential value, a model or runner choice for Kapso’s own agents, an engine event, a memory of past tasks, or any field the engine would have to parse. Domain commands, API connections and model names inside domain content are fine. The harnesses Kapso ships are held to this by a scan of our own (benchmark tooling, not check): no benchmark, task, dataset or competition name, no data library’s name on a page, no number pinned beside a quota, deadline, score or hardware word — a hit is a file, a line and a word.

Harness reference

Every form, rule, file and field

The trading example

Forty files, and the run that improves them
Related pages: What a harness is · Use a harness · CLI reference · Configuration Kapso is an open-source framework by Leeroo that builds software toward measurable goals through experiment campaigns. Source code: github.com/Leeroo-AI/kapso · Install: pip install leeroo-kapso · Every page as plain text: llms.txt.