Vibe Code Textbook

Harnesses · · 1,396 words · 6 min read

What is a coding-agent harness? The loop, tools, gate, and context

A coding-agent harness is the loop around the model. Here is what each part does, read from three real harnesses, with a 100-line runnable version you can inspect.

harness agent loop permissions context window

You get a working definition of the word "harness", a walk through the four parts every coding agent has, with each part checked against a real implementation, and a runnable Python file that does the whole thing in about a hundred lines with a scripted stand-in where the model would be.

The word itself

The Claude Code documentation uses the term directly. Its architecture page says the agentic loop "is powered by two components: models that reason and tools that act", and that "Claude Code serves as the agentic harness around Claude: it provides the tools, context management, and execution environment that turn a language model into a capable coding agent." That sentence is the whole definition. A model on its own returns text. A harness gives it a way to act, a place to act in, and a memory of what it has done so far.

The same page describes the loop as three phases that "blend together": gather context, take action, verify results. It gives a concrete trace for "fix the failing tests": run the suite, read the errors, search for the source files, read them, edit them, run the suite again. Every one of those six steps is a tool call whose output goes back into the conversation before the next decision.

Part one: the loop

The smallest honest example I know is mini-swe-agent, whose README at commit 04d809ceab9d calls it "just some 100 lines of python for the agent class". Its default agent, in the file default.py at the same commit, has a run() method that seeds the messages with a system prompt and the task, then calls step() until a message with the role exit appears. Each step queries the model, parses an action out of the reply, hands it to env.execute(), and appends the observation. Three other things end the run: a step limit, a cost limit, and a wall-time limit, each checked before the next model call.

Two design choices in that file are worth copying. The history is linear: the README says "every step of the agent just appends to the messages and that's it", so the trajectory and the prompt are the same object. And each action runs as an independent subprocess.run, so there is no stateful shell to lose track of.

OpenHands documents the same loop with more ceremony. Its SDK architecture page for the agent says "each step() call processes one reasoning cycle", and lists the order inside a step: execute any actions already approved, condense history if a condenser is configured, query the model, parse the reply into action events or message events, stop and set the conversation status to WAITING_FOR_CONFIRMATION if an action needs approval, otherwise execute the tools and record observation events. The agent is described as "Stateless: Agent holds no mutable state between steps", which is the same property as the linear history in mini-swe-agent, arrived at from the other direction.

Part two: the tools

A tool is a named function with a schema the model can see. The Model Context Protocol specification makes the shape explicit for tools that live outside the harness: tools are discovered with a tools/list request and invoked with tools/call, both JSON-RPC 2.0 messages, and each tool definition carries a name, a description, and an inputSchema in JSON Schema. Built-in tools in a harness look the same from the model's side, even when they are ordinary function calls in-process.

Claude Code groups its built-in tools into five categories on the architecture page: file operations, search, execution, web, and code intelligence. The mini-swe-agent README goes the other way and gives the model a single tool, bash: it "does not have any tools other than bash" and "doesn't even need to use the tool-calling interface of the LMs". Both are harnesses. The difference is how much structure the harness imposes on the model's actions, which is the subject of the SWE-agent reading at /posts/swe-agent-agent-computer-interface-explained.html.

Part three: the permission gate

Between the model deciding on an action and the action running, every serious harness has a gate. In OpenHands it is the WAITING_FOR_CONFIRMATION status above. In Claude Code it is the permission system: the architecture page says Manual mode "asks before file edits and shell commands", and that you can "allow specific commands in .claude/settings.json so Claude doesn't ask each time". The full comparison across harnesses, with the exact rule syntax each one uses, is in /posts/coding-agent-permission-models-compared.html.

The gate is not the same thing as the model's judgement. The MCP specification puts it bluntly in its security section: "Tools represent arbitrary code execution and must be treated with appropriate caution", and "Hosts must obtain explicit user consent before invoking any tool". A harness that lets the model's own opinion decide what runs has no gate.

Part four: the context

The context is the message list, and it is finite. The Claude Code context-window page runs a simulated session and attaches token figures to each item that loads. In that simulation the system prompt costs 4,200 tokens, the project CLAUDE.md 1,800, auto memory 680, environment info 280, and a single file read of an auth module 2,400. The page's numbers are illustrative for one example session, so treat them as orders of magnitude, but the shape is what matters: a few thousand tokens before you type anything, and every file read or command output added on top.

When the window fills, the harness has to throw something away. The architecture page describes the order: "It clears older tool outputs first, then summarizes the conversation if needed. Your requests and key code snippets are preserved; detailed instructions from early in the conversation may be lost." OpenHands does the same job with a condenser that, per its docs, triggers when the event count passes a threshold (default 120) and keeps the first few events verbatim (default 4). Both are the same idea: old tool output is the cheapest thing to lose.

The hundred-line version

The shipped file what-is-a-coding-agent-harness.py has all four parts and nothing else. The model is a scripted list of actions, and the file says so in its header, because the point is to see the harness, not the model. Here is the gate and the loop body:

DENY = [("run", "rm "), ("run", "git push")]
ASK = [("write_file", "")]
ALLOW = [("read_file", ""), ("run", "python "), ("run", "dir"), ("run", "ls")]


def gate(tool: str, arg: str) -> str:
    for rules, verdict in ((DENY, "deny"), (ASK, "ask"), (ALLOW, "allow")):
        if any(t == tool and arg.startswith(p) for t, p in rules):
            return verdict
    return "ask"

Deny is checked first, then ask, then allow, and the default for anything unmatched is to ask. That order is the one the Claude Code permissions docs describe for their rules, and it is the only order that makes a deny rule mean anything.

The trimmer is four lines:

def trim(messages: list[dict]) -> None:
    while sum(len(m["content"]) for m in messages) > CONTEXT_BYTES:
        old = next((m for m in messages if m["role"] == "tool" and m["content"] != "[trimmed]"), None)
        if old is None:
            return
        old["content"] = "[trimmed]"

It measures bytes instead of tokens, which is a heuristic, and it replaces the oldest tool result first, which is the policy the real harnesses describe.

What ran here

I ran the file with Python 3.13.12 on Windows from the site folder. The scripted model writes a file, runs it, tries a destructive command, reads the file back, produces a deliberately large output, runs the file again, and declares itself done. This is the exact output:

  [ask] write_file('hello.py') -> approved by policy (demo)
step 1: write_file('hello.py') [ask] -> 'wrote 22 bytes to hello.py'
step 2: run('python hello.py') [allow] -> '45'
step 3: run('rm -rf /') [deny] -> 'permission deny: run rm -rf /'
step 4: read_file('hello.py') [allow] -> 'print(sum(range(10)))\n'
step 5: run('python -c "print(\'.\' * 900)"') [allow] -> '............................................................'
  [context] trimmed the oldest tool result
  [context] trimmed the oldest tool result
  [context] trimmed the oldest tool result
  [context] trimmed the oldest tool result
  [context] trimmed the oldest tool result
step 6: run('python hello.py') [allow] -> '45'
step 7: done -> hello.py prints 45
context: 15 messages, 480 bytes

Step 3 shows the gate doing its one job: the denied command never reaches the shell, and the model gets the denial as its observation. Step 5 shows the context budget: a 900-character output pushed the message list over the 1,200-byte limit, and the trimmer replaced five old tool results before the next step. The budget is set absurdly small so that this happens in a demo; a real harness would be working in tokens against a window in the hundreds of thousands.

What this file is not

It is not an agent. Nothing in it reasons. The value of running it is seeing that the loop, the dispatch table, the gate, and the trimmer are separable and small, and that everything interesting about a real harness is a policy decision inside one of those four boxes: which tools exist, what the gate allows without asking, what gets trimmed first, and when the loop stops. The stopping rules deserve their own article, at /posts/when-to-stop-a-coding-agent.html.

What I did not verify

I read the OpenHands and Claude Code behaviour from their documentation pages, not from their source, and I read mini-swe-agent's default.py from the raw file at the commit named above. I did not run mini-swe-agent or OpenHands in this session. The token figures from the context-window page are the page's own simulation, not a measurement of any session of mine.

Code and data

Sources

  1. Anthropic, "How Claude Code works" (Claude Code docs, read 2026-09-05)
  2. SWE-agent/mini-swe-agent, "src/minisweagent/agents/default.py" at commit 04d809ceab9d (read 2026-09-05)
  3. SWE-agent/mini-swe-agent, "README at commit 04d809ceab9d" (read 2026-09-05)
  4. OpenHands, "Agent" (Software Agent SDK architecture docs, read 2026-09-05)
  5. Model Context Protocol, "Specification 2025-06-18" (read 2026-09-05)
  6. Anthropic, "Explore the context window" (Claude Code docs, read 2026-09-05)