A runtime and console for coding agents

The agent said done. The file is missing.

The Harness runs coding agents with explicit permissions and budgets. It records the run and checks the outputs you require before calling it complete.

Get started Read the source

Runs on your machine · Python 3.14 · Postgres · your agent CLI

Documented CLI example
harness run "write resumen.md with three lines about X" --expect resumen.md

incomplete: 6 events recorded
  outstanding: output resumen.md is missing
  the runtime said completed; the closure is derived from what exists
The runtime said completed
The Harness derived incomplete output resumen.md is missing

with fs.write

harness run "write resumen.md with three lines about X" \
    --grant fs.write --expect resumen.md

  17:04:02  started          claude
  17:04:05  asked for        Write(file_path=resumen.md)  [the runtime's own tool]
  17:04:06  came back        177 characters
  17:04:08  spent            in=4 out=330 cache=6457
  17:04:09  finished         succeeded

A separate workspace. A grant allows the write; it does not make an agent produce the file. Two recorded examples, not a prediction.

This example checks for a non-empty file, not the quality of its contents. Both examples in the README

01 · closure

Two answers to "is it done?"

An agent that reports its own success is a system with no ground truth. The Harness keeps what the model believed beside the closure, never in its place, and says which obligation is still open.

Field What the runtime said What exists
Status completed incomplete
Based on its last message the workspace and the log
resumen.md not looked at missing
Kept as runtime_said, recorded the task's closure

02 · lifecycle

You state what is owed. The closure checks it.

Four things happen to every run, in this order. Only the last one decides how it ended.

  1. harness run "…"

    Objective

    Your words, plus what the task owes: a file, a command that must pass, a criterion and who judges it.

  2. --grant · --budget-*

    Grants and budget

    Nothing is granted by default. Grants become the tool list the runtime starts with. Limits on tokens, seconds, dollars and tool calls.

  3. control.audit_event

    Recorded execution

    Each event is appended and committed on its own, so a run can be read while it happens and after it crashes.

  4. derive()

    Closure

    Computed from what exists: the step ended and every obligation holds. The worker may not write a task's ending; control does.

Without an obligation nothing is owed, and finishing is the runtime's word.

03 · capabilities

What a run is held to.

Each piece below is a flag, a table or a process you can read in the source.

Obligations

Checks run after the runtime stops. An independent check is pinned by its sha256 when the run is admitted.

--expect FILE
a file the task owes, non-empty and inside the workspace
--check COMMAND
a command that must pass, run by the harness after the runtime stops
--check-independent PATH
a script pinned by its sha256 when the run is admitted; changed bytes fail the check
--criterion JUDGE: STATEMENT
what must be true, and who decides: check, reviewer or person
--review
a second runtime reads the changes without the run's own account and judges them

Grants

Named per task, repeatable, none by default. shell.run gets the runtime's own sandbox with the home folder denied.

  • fs.read
  • fs.write
  • source.read
  • shell.run
  • net.read

--grant fs.write

Append-only log

A trigger refuses UPDATE and DELETE on the audit table. A database rule, not a cryptographic proof.

  1. 17:04:02 started
  2. 17:04:05 asked for Write
  3. 17:04:06 came back
  4. 17:04:08 spent
  5. 17:04:09 finished

update control.audit_event set kind = 'rewritten';
ERROR: control.audit_event is append-only

Budgets

Limits a run is admitted with. A run that reaches one ends and says which.

  • --budget-tokens
  • --budget-seconds
  • --budget-usd
  • --budget-tools

Illustrative levels

Plans first

With --plan the run writes a plan and waits. The planner holds only reading tools, and a worker carries out only the exact plan approved.

[a] carry it out   [c] change it   [r] reject   >

Three processes, if you want them

With --detach the work splits the way the design calls for. Without a worker, a detached task waits in the queue.

  • harness worker --grant fs.writetakes queued jobs and does them
  • harness controlwrites the endings the worker may not
  • harness run "…" --detachqueues one and returns immediately

Providers through the bridge

Reading tools only, each call leaving a receipt. A tool that appears after you approved a provider is drift and publishes nothing. Credentials are secret:// references, never values in the database.

  • GitHubrepositories, issues, pull requests
  • Linearissues and projects
  • Context7documentation for libraries
  • Grafanadashboards, datasources, alerts
  • Serenasymbols and references across a repository
  • Argo CDwhat is deployed and whether it is in sync
  • Kargostages, freight and promotions

04 · console

Read the run, not the log.

The console answers what is running, what is waiting on you, what a run produced and what it cost. A run opens on its closure and the reason for it.

Example run Static HTML from the console's patterns and three documented runs. Not a connected console.
  • Inbox
  • Runs
  • Plans
  • Providers
  • Spend
  • System

run · claude

write resumen.md with three lines about X

incomplete output resumen.md is missing
Runtime said
completed
Obligations
resumen.md · output · missing
Grants
none
Events
6 recorded
Cost
not part of this example

the runtime said completed; the closure is derived from what exists

run · claude

write resumen.md with three lines about X

succeeded resumen.md written

Timeline

  1. 17:04:02 started claude
  2. 17:04:05 asked for Write(file_path=resumen.md) [the runtime's own tool]
  3. 17:04:06 came back 177 characters
  4. 17:04:08 spent in=4 out=330 cache=6457
  5. 17:04:09 finished succeeded
Grants
fs.write
Cost
not part of this example

run · claude · bridge

Llama a mcp__harness__grafana__list_datasources y escribe datasources.md con los primeros cinco: nombre y tipo.

succeeded datasources.md exists

Receipts

  • ✓ grafana.list_datasources
  • ✓ fs.write
Grants
grafana.read · fs.write
Cost
not part of this example

Source

In the console, j and k move between rows, Enter opens one and ⌘K opens the palette.

05 · permissions

What a grant controls, and what it does not.

A granted tool can run in two ways. Each row of the log says which one happened.

Native tools compared with bridged tools
Native toolsBridged tools (--bridge)
Who runs the toolThe runtime, in its own process.The Harness, through an MCP server it runs for the length of the run.
When the grant appliesAt start: the grants become the tool list the runtime is given.On each call, before the effect, against the path the argument resolves to.
What the log saysasked for, never authorizedwhat was checked, and what it did
ReceiptNoneOne per call

A bridged write and its receipt, from a separate documented run

tool    | fs.write@1.0.0
status  | success
effect  | applied
receipt | {"reference": "/…/informe.md"}

Source

Limits, stated plainly

  • This contains the tools, not the process. Unless you name allowed domains, a run's shell can reach any host.
  • shell.run uses the runtime's own sandbox. Linux needs bubblewrap and socat for it; without a sandbox the run fails instead of running the shell unsandboxed.
  • The audit table refuses UPDATE and DELETE through a database trigger. Whoever administers the database can change that.
  • Capabilities differ by runtime. Codex and Kimi are admitted only with the grants their own behavior needs.

What the grants do, and do not do

06 · runtimes and cost

It drives the agent CLI you already use.

The Harness runs an agent CLI on your machine, with your account for that CLI. It does not supply a model. Each CLI is pinned to an exact version, and the doctor checks it.

  • Claude claude pinned 2.1.290 The runtime of every example on this page.
  • Codex codex pinned 0.159.2 Runs commands in its own sandbox, so it is admitted only with shell.run.
  • Gemini gemini pinned 0.62.0 Adapter present. Not shown in an example here.
  • Kimi kimi pinned 0.36.1 Runs every tool on its own in prompt mode, so it is admitted only with shell.run and fs.write.

Pinned versions in toolchain.toml

Each cost keeps its source.

A figure the runtime reported, an estimate from a price table, or unknown. Unknown stays unknown and is counted as unreported, never as $0.

Three kinds of cost figure. Illustrative values.
KindShown asMeaning
Reported$0.41The runtime's own figure for the turn.
Estimated≈ $0.38Tokens priced from the table for that model and date.
UnknownunknownNo figure and no price. Counted as unreported.

07 · install

Install it and run the example.

Checked on macOS with the commands below, using a scripted runtime in place of Claude. Linux uses the same commands; there, shell.run also needs bubblewrap and socat, and this page has not checked that path.

  1. Get the code and start Postgres

    git clone https://github.com/andresgarcia29/the-harness
    cd the-harness
    uv sync
    docker compose up -d --wait
  2. Keep the two secrets in files

    Each file is created only if it is absent, readable by you alone. The database URL is the local compose default. The files keep the secrets out of the environment every child process can read.

    dir=~/.config/harness-secrets
    mkdir -p "$dir" && chmod 700 "$dir"
    [ -e "$dir/database_url" ] || (umask 077; printf %s 'postgresql://harness:harness@127.0.0.1:55433/harness' > "$dir/database_url")
    [ -e "$dir/token_secret" ] || (umask 077; openssl rand -hex 32 | tr -d '\n' > "$dir/token_secret")
    export HARNESS_DATABASE_URL_FILE="$dir/database_url"
    export HARNESS_TOKEN_SECRET_FILE="$dir/token_secret"
  3. Apply the migrations and ask the doctor

    Each check reads ready, missing or unknown. A line that is not ready says what to do next, and unknown is not ready.

    mkdir -p artifacts
    .venv/bin/harness control --once
    .venv/bin/harness doctor
  4. Run the objective in two new workspaces

    Expect incomplete for the first, with output resumen.md is missing. The second may write the file: the agent decides. The real runs call Claude.

    without=$(mktemp -d)
    .venv/bin/harness run "write resumen.md with three lines about X" \
      --workspace "$without" --expect resumen.md
    
    with=$(mktemp -d)
    .venv/bin/harness run "write resumen.md with three lines about X" \
      --workspace "$with" --grant fs.write --expect resumen.md
    ls "$with"
  5. Read it back

    show prints the last run. The console prints a local address with a one-time admin token and opens it.

    .venv/bin/harness show
    npm --prefix web ci && npm --prefix web run build
    .venv/bin/harness console

08 · questions

Questions

What does succeeded mean?

The step ended and every obligation holds: each expected file exists and each required check passed. Without an obligation, succeeded means the runtime finished. Source: src/harness/control/completion.py

What does --expect check?

That a non-empty regular file with that name is inside the workspace when the run closes. It does not judge the contents, and it does not prove this run wrote the file. Start each example in a new workspace. Source: src/harness/worker/service.py

Is a grant a sandbox?

No. Native grants shape the tool list a runtime starts with. The bridge checks each call it carries. Neither contains the process or its network. Source: README.md

Which runtimes are supported?

There are adapters for claude, codex, gemini and kimi. Each has its own capabilities and admission rules, and the examples here use Claude. Source: src/harness/worker/backends/external/cli.py

What happens if a run crashes?

Each event is committed on its own, so what was written stays readable. A detached task held by a worker that dies is taken over when its lease expires. Source: docs/install.md

Where do data and credentials live?

In the Postgres you run and in files you own. A provider credential is stored by name as a secret:// reference, never as a value in the database. Source: docs/install.md

Give it an objective. Inspect the result.

Get started Read the source