> ## Documentation Index
> Fetch the complete documentation index at: https://docs.leeroo.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Write a harness

> From the two-file starter kapso harness new writes to a harness with pages, skills, tool servers, delegates and hooks: what goes in each file, what check enforces, and how a session on either coding-agent CLI sees it.

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

```bash theme={null}
kapso harness new trading
```

```text theme={null}
created /home/me/trading
  harness.yaml                 name, version, description; the module lists to fill
  instructions/guardrails.md   the rules every module's sessions read
next: edit the two files, then  kapso harness check ./trading
```

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

```text theme={null}
trading/
├── harness.yaml          name, version, description, environment, modules
├── instructions/
│   ├── guardrails.md     every module: standing rules, where to look
│   └── <module>.md       one module: setup, ideation, implementation, verification, feedback, deployment, learning
├── skills/<name>/        SKILL.md with front matter; scripts under it
├── agents/<name>.md      a delegate: front matter, then its instructions
├── mcp.json              tool servers by name, stdio or streamable-http
├── servers/              the stdio servers' code, when the harness ships them
├── hooks/<name>/         hook.json in the host's hooks format, with its script beside it
└── requirements.txt      what the bundled tooling needs
```

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

```yaml theme={null}
schema_version: 1
name: trading                       # [a-z0-9][a-z0-9-]*; the directory's name
version: 0.1.0                      # bump when behaviour changes; recorded with every run
description: Research, implement, verify and deploy systematic trading strategies.

environment:                        # prerequisites of the machine; reported, never installed
  python: requirements.txt          # a requirements file, checked against python3 on PATH
  commands: []                      # programs that must be on PATH
  gpu: false

modules:                            # any of the seven; a module left out still gets its pages
  implementation:
    skills: [backtesting, market-data, risk-limits, lookahead-scan]
    mcp_servers: [market-data]
    agents: [lookahead-auditor]
    hooks: [session-brief, deny-web-fetch, deny-web, protect-prepared, deny-snapshot, scan-writes, require-results]
```

`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.

| Page | What it holds |
| - | - |
| `guardrails.md` | The rules that hold for every task in the domain, then the skills, servers and delegates a session should know exist |
| `setup.md` | What to pin before the first experiment, and what the domain statement must record for this task |
| `ideation.md` | The families of approaches, the axes worth exploring, what is known not to work |
| `implementation.md` | The discipline every implementer keeps, the scripts to run before the evaluation, the files to leave behind |
| `verification.md` | The list a verifier checks, with what: scripts, delegates, the evidence a finding needs |
| `feedback.md` | What the judge weighs beside the score: the risk line, the rules it must see kept |
| `deployment.md` | What a deployment of this domain's software needs |
| `learning.md` | What is worth a lesson in this domain, and what is noise |

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.

```markdown theme={null}
---
name: lookahead-scan
description: "Scan a strategy for look-ahead: centred windows, future shifts, whole-series statistics. Use when a signal is written or changed, before the backtest."
---

Run the scan on the attempt's diff; a hit is a finding with its file and line:

    git diff main...HEAD | python3 $KAPSO_HARNESS_ROOT/skills/lookahead-scan/scripts/scan.py
```

`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:

```json theme={null}
{
  "mcpServers": {
    "market-data": {"command": "python3", "args": ["${PLUGIN_ROOT}/servers/market_data.py"]},
    "broker": {"command": "python3", "args": ["${PLUGIN_ROOT}/servers/broker.py"], "env": {"BROKER_API_KEY": "${BROKER_API_KEY}"}},
    "docs": {"type": "streamable-http", "url": "https://docs.example.com/mcp", "headers": {"Authorization": "${DOCS_AUTHORIZATION}"}}
  }
}
```

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.

```markdown theme={null}
---
name: lookahead-auditor
description: Audits one attempt for look-ahead and reports each finding with its file, line and the bars it would read ahead.
---

You audit one attempt ...
```

## 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.

```json theme={null}
{
  "hooks": {
    "PreToolUse": [
      {"matcher": "Bash", "hooks": [{"type": "command", "command": "python3 ${PLUGIN_ROOT}/hooks/deny-web-fetch/deny_web_fetch.py"}]}
    ]
  }
}
```

```python theme={null}
event = json.load(sys.stdin)
command = event.get("tool_input", {}).get("command", "")
if not FETCHERS.search(command):
    sys.exit(0)
print(json.dumps({"hookSpecificOutput": {
    "hookEventName": "PreToolUse", "permissionDecision": "deny",
    "permissionDecisionReason": "Web fetches are off in this harness: all market data comes from the market-data server's pinned snapshot.",
}}))
```

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:

| | Claude Code | Codex |
| - | - | - |
| the shell tool | `Bash`, `tool_input.command` | `Bash`, `tool_input.command` |
| the file tools | `Write`, `Edit`, `MultiEdit`; `tool_input.file_path` | `apply_patch`; `tool_input.command` is the patch, whose file headers are `*** Add File: <path>`, `*** Update File: <path>`, `*** Delete File: <path>` and `*** Move to: <path>` (one regex in the example's `protect-prepared` reads all four); a matcher `Edit\|Write\|MultiEdit` fires on it too |
| a server's tool | `mcp__<server>__<tool>` | `mcp__<server>__<tool>` |

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

```bash theme={null}
kapso harness check ./trading
```

`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](https://docs.leeroo.com/docs/reference/harness.md#check) 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](https://docs.leeroo.com/docs/harnesses/example-trading.md) 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.

## Related

> [card] [Harness reference](https://docs.leeroo.com/docs/reference/harness.md)
>
> Every form, rule, file and field

> [card] [The trading example](https://docs.leeroo.com/docs/harnesses/example-trading.md)
>
> Forty files, and the run that improves them

Related pages: [What a harness is](https://docs.leeroo.com/docs/harnesses.md) · [Use a harness](https://docs.leeroo.com/docs/harnesses/use.md) · [CLI reference](https://docs.leeroo.com/docs/reference/cli.md#kapso-harness) · [Configuration](https://docs.leeroo.com/docs/reference/configuration.md#harness)

Kapso is an open-source framework by [Leeroo](https://leeroo.com) that builds software toward measurable goals through experiment campaigns. Source code: [github.com/Leeroo-AI/kapso](https://github.com/Leeroo-AI/kapso) · Install: `pip install leeroo-kapso` · Every page as plain text: [llms.txt](https://docs.leeroo.com/llms.txt).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.