Harness Engineering
The model returns text. Everything else — what goes in, whether the action happens at all, who stops it, and how the agent knows it succeeded — is code somebody wrote.

The previous article was about context engineering. It sidestepped one question that sits underneath everything else: who decides what goes into the context?
Not the model. The model receives a window and returns text. Everything else — what went in, whether its output has any effect on the world, whether it gets called again, and when it stops — is decided by the code around it.
That code has a name. It's called the harness, and designing it is a discipline of its own.
We'll stay with the same example: an agent migrating two hundred files from one library to another. Context engineering asked what the agent sees on file 60. Harness engineering asks something else:
- How does the agent know file 59 actually works?
- What happens when file 61 breaks the build?
- Who stops it if it starts deleting?
- Who continues from file 140 if the process dies?
- Who decides which answer is right when two subagents come back disagreeing?
None of these questions is about the model.
The Model, the Context, and the Harness
One comparison from the workplace sorts all three out at once.
The model is the capability. Your company hires a highly intelligent, well-educated person — that qualification doesn't change with where they work.
The context is the onboarding. For the first two weeks they're given documentation to read, the processes are explained, they're shown the design problems and why some module looks strange. This doesn't make them smarter — it makes them informed about this particular workplace.
The harness is everything else in the company. The laptop, access to the software, production permissions, CI, code review, the fact that somebody approves the deploy, the definition of "done," the retrospective.
That third thing isn't passive infrastructure the employee reaches for when needed. It determines what can happen at all.
The harness decides when the model gets called. It decides what it sees — meaning all of context engineering happens inside the harness, not next to it. It decides whether a proposed action gets executed or held for approval. It decides when the work is finished. The model can't refuse any of these decisions, because it has no idea they're being made.
The model doesn't use the harness. The harness uses the model.
And from here comes the conclusion that makes the discipline worth having:
A brilliant new hire with no laptop, no access to the code, no way to run tests, and nobody to tell them whether their work is any good produces nothing. Nobody would say "we need a smarter employee." Everybody would say the organization is broken.
With agents, we say "we need a better model" surprisingly often.
The most underrated item on that list is perception — whether the agent can see the result of its own action at all. An agent that acts but can't see what happened is one of the most common failures in the entire discipline, which is why it gets its own section shortly.

What a Harness Is
The harness is the runtime layer around the model: the loop, the tools, the state, the feedback, the gates, recovery, termination, observability, and orchestration once there's more than one agent.
The shortest operational definition, though, is this: the harness is the loop that turns a model call into a working process.
messages = [{"role": "user", "content": task}]
while True:
response = client.messages.create(
model="claude-opus-5",
max_tokens=16000,
tools=tools,
messages=messages,
)
if response.stop_reason == "end_turn":
break
messages.append({"role": "assistant", "content": response.content})
results = []
for block in response.content:
if block.type == "tool_use":
results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": execute(block.name, block.input),
})
messages.append({"role": "user", "content": results})
Fifteen lines. That's a working agent — and it's the entire harness in its most primitive form.
What's worth noticing is what is not in those fifteen lines but has to be there in production:
execute()runs whatever the model asks for, without asking anyone;- nothing checks whether the action succeeded — the result is passed along naively;
- on an error the whole loop blows up and hours of work vanish;
- there's no stopping condition beyond the model's goodwill;
- if the process dies on file 140, everything starts from zero;
- nobody outside can see what happened inside.
Harness engineering is filling in that list. Every section that follows is one of its lines.
The loop doesn't have to be written by hand — SDKs offer a ready-made tool runner, and there are platforms that host it for you. But the decisions stay with whoever is building the system. A ready-made loop saves you the
while, not the architecture.
Three Kinds of Failure
This is the most practical part of the article, because a lot of the time lost in agentic systems goes into treating the wrong layer.
| Layer | The failure is | Symptom |
|---|---|---|
| Model | reasons badly given good information and working tools | the logic is wrong, but the actions were executed and observed |
| Context | reasons well, but over wrong, stale, or poisoned information | confidently does something that was true thirty steps ago |
| Harness | can't act, can't see the result, isn't stopped in time, or its work is lost | claims it's done; repeats the same action; stops halfway |
Diagnosis by symptom:
| What you observe | Most likely |
|---|---|
| The agent says "done" and nothing works | harness — no feedback |
| Repeats the same failing action | harness — the error never reaches it |
| Deletes or deploys something it shouldn't have | harness — missing gate |
| Stops halfway and declares success | harness — no definition of "done" |
| Work disappears on restart | harness — the state lived in the window |
| Picks the wrong one out of fifty tools | context — confusion |
| Uses an API that doesn't exist | context — poisoning… or harness (see below) |
| Does something stupid with complete and correct information | model |
The second-to-last row deserves attention, because it connects the two articles.
In the article on context engineering, poisoning was described as a context failure: the agent invents a signature on file 12 and reuses it through file 90. That's true — but it's also a harness failure. If there's a type checker in the loop, the invented signature dies on file 12 and never enters the history.
From which follows a principle that holds surprisingly widely:
Good feedback reduces the need for context engineering.
An agent that can verify doesn't carry assumptions forward. A large share of the accumulation we fight with compaction and clearing exists only because the agent had to remember things instead of being able to check them.

The Patterns
1. Feedback — the most important thing here
An agent without feedback doesn't work. It generates. The difference is that generating looks equally successful whether it's right or not.
If the agent writes code and never runs it, it will report success. Not because it's lying, but because it has no way to know otherwise. Its output is text that looks like working code, and that's the entire body of information it has.
So the first question about any agent isn't "what tools does it have," but "how does it know it succeeded?"
The levels, in increasing order of value:
| Level | Instrument | Catches |
|---|---|---|
| None | — | nothing; the agent always "succeeds" |
| Syntactic | linter, parser | broken code |
| Type | type checker, compiler | non-existent APIs, wrong signatures |
| Behavioral | tests | wrong logic |
| Semantic | a human or a second agent | the wrong task, correctly executed |
For the migration: an agent with tests in the loop works. An agent without tests produces two hundred files that look migrated.
The key implementation detail is that the error has to come back as a result, not be thrown as an exception:
def execute(name, args):
if name == "migrate_file":
write_file(args["path"], args["content"])
result = run_tests(args["path"]) # ← the harness checks
if result.failed:
return {
"content": f"Tests failed:\n{result.output}",
"is_error": True, # ← and hands the failure back
}
return {"content": "OK, tests pass"}
A thrown error interrupts the normal agent loop unless the harness catches it and turns it into a tool result. An error returned as a structured result is information the model can act on — it sees what failed and corrects.
A rule worth remembering: give the agent the same feedback a human would get. Nobody onboards a developer by forbidding them to run the tests.
2. Tool design — what the harness can intercept
Here lies the most underrated decision in the whole discipline.
The model doesn't know where the security boundary runs, what the approval policy is, or what the interface looks like. It emits action requests. The shape of those requests determines what the harness can do with them at all.
bash gives maximum breadth. The agent can do almost anything. But what the harness is left with is an opaque string — the same shape for every action:
{ "name": "bash", "input": { "command": "rm -rf ./generated && npm run build" } }
A specialized tool gives typed arguments a decision can be made on:
{ "name": "delete_path", "input": { "path": "./generated", "recursive": true } }
The second form can be held for approval, logged, rendered in an interface, flagged for parallel safety. The first can be controlled too, but that now requires analyzing the shell command — a far harder and less reliable boundary.
When an action is worth its own tool:
- Security boundary. Something that has to pass through approval.
- Staleness check. A specialized
editcan refuse a write if the file changed since the agent last read it. With a genericbash, enforcing that constraint structurally is considerably harder — and it's worth noticing that here the harness prevents a context failure. - Rendering. Actions that need to be shown to a human in a particular way.
- Parallelism. The harness can run concurrently the things it knows are safe. If everything is wrapped in generic shell commands, classifying those actions becomes much harder.
The practical rule: start with bash for breadth, and promote to a separate tool whatever needs to be gated, displayed, audited, or parallelized.
3. Gates — the criterion is reversibility
The temptation is to gate on "danger." A more useful axis is a different one.
How much does it cost to undo?
Cheap to undo — let it run. Expensive or impossible — gate it, no matter how harmless it looks.
| Action | Reversible? | Gate |
|---|---|---|
| Editing a file in git | yes, easily | no |
| Running tests | yes, nothing changes | no |
rm -rf outside the working directory |
no | yes |
| Sending an email | no | yes |
| Deploying to production | sometimes, expensively | yes |
| A call to a paid API | no — the money is spent | depends on the amount |
Note that "sending an email" can be more dangerous than deleting a git-tracked file, even though it sounds more harmless. The file can be restored. The email has already left.
Two practical details: the gate has to live in the harness, not in the prompt — an instruction to "always ask before deleting" is a recommendation, not a constraint. And a refusal has to carry a reason, so the agent can try something else instead of banging into the same wall.
4. Recovery — state must not live in the window
If the process dies on file 140 and the work starts from zero, the harness is broken.
This is where the two articles meet directly. The "write it out" pattern from context engineering isn't only about saving tokens — it's the precondition for recoverability. The progress file survives a restart. The context window doesn't.
The minimum is three things kept outside the window: how far the work has got, what has been learned along the way, and what's next. If that lives in durable storage, a restart costs one read.
A related question is retries. The difference that matters:
- Transient error — network, rate limit, timeout — the harness can retry on its own without bothering the model. Nothing needs to enter the context.
- Meaningful error — a test failed, a file doesn't exist — goes to the model, because it's information that has to be reasoned about.
Conflating the two is a common defect: transient errors enter the window as content and poison it, while meaningful ones get retried mechanically and never corrected.
5. Termination — agents don't know when to stop
Three different failures hide under one word.
Stops too early. Declares success on file 60. Cured with a checkable definition of "done": not "when the model says so," but "when 200 out of 200 files have passing tests." The definition of done belongs to the harness.
Doesn't stop at all. Spins the same loop. The harness can detect repetition — the same action three times with the same result is a signal to interrupt, not to try a fourth time.
Runs out of resources halfway. max_tokens is a limit on one specific model response. But the task has another budget too: time, tokens, steps, money, or number of tool calls. That budget belongs to the harness.
For example:
MAX_STEPS = 200
MAX_COST = 10.00
while not done:
if steps >= MAX_STEPS or cost >= MAX_COST:
checkpoint_state()
break
response = call_model(...)
cost += estimate_cost(response)
steps += 1
The difference matters. If you rely only on the limit of a single response, the model can simply be cut off mid-work. If the harness tracks the budget of the whole task, it can stop in a controlled way, record the progress, and leave the process in a recoverable state.
6. Observability — you can't fix what you can't see
An agent is a non-deterministic process that may make hundreds of decisions. When it goes wrong, the only way to understand why is to see what it saw and what happened next.
The minimum per step: what entered the context, what action the model proposed, what the tool returned, how much time and resources it cost. Without that, debugging is guesswork and "it works better today" isn't engineering.
The useful metrics depend on the system, but a few are almost always worth having: how many steps a task takes, how many tool calls fail, how many retries there are, how often a gate is reached, what a successful task costs, and how the context cache is being used.
The first few catch loops and wasted work. The last one can reveal quiet changes in the context that destroy the cache hit rate and raise the cost without the behavior visibly changing.

7. Orchestration — when there's more than one agent
If the task decomposes into independent parts, each can get its own window. In the previous article that was "Isolate" — one of the four verbs, treated as a way to save context. Here is its other side: isolation is a context gain, but orchestration is harness work. Who spawns a subagent, what it sees, what it returns, and who decides what all of it means — none of that happens in the model.
For the migration: one subagent per file, or per group of files. Each gets its file, the rules from the progress file, and nothing else. The parent never sees the contents of the two hundred files — it sees two hundred lines of results.
This is the pattern that can multiply capacity rather than merely stretching it. The other six conserve it. And it's the only one where the harness can fail in a way that simply doesn't exist with a single agent.
The four decisions
What the subagent sees. It starts from zero. Whatever it needs has to be handed to it — that's the briefing, and it's the most common failure. An under-briefed subagent rediscovers everything itself and costs more than it saves.
What it's allowed to do. Permissions are granted per subagent, not globally. An agent that only reads and summarizes has no need for write access. Section 3 applies again here, at a different level.
What it returns. If five subagents return free-form text, the parent has to read five essays — you've moved the context problem, not solved it. The return contract is a concrete harness lever:
RESULT_SCHEMA = {
"type": "object",
"properties": {
"file": {"type": "string"},
"status": {"enum": ["ok", "failed", "skipped"]},
"tests_pass": {"type": "boolean"},
"new_rule": {"type": ["string", "null"]}, # a rule, if one was learned
"blocked_by": {"type": ["string", "null"]},
},
"required": ["file", "status", "tests_pass"],
}
Five fields instead of five paragraphs. The parent can process them without reading them.
What happens on failure. One of the five dies. Does everything fall over? Does it retry? Does it continue with four and flag the missing one? That decision has to be made in advance — the default is usually "it disappears quietly."
The merge problem
Here's the question that has no analogue with a single agent.
Two subagents migrate different files. Both encounter .timeout(ms). The first is working on a file where the timeout merely bounds the request — it concludes the equivalent is AbortSignal.timeout(ms). The second is working on a file where the timeout has to be cancellable from outside — it concludes a manual AbortController is needed.
Both are right. For their own file.
The parent receives two rules that contradict each other and writes one of them into the progress file. From there on, a hundred and fifty files get migrated by a rule that's correct for half of them.
Notice that no subagent hallucinated, nobody made a mistake, and no context was poisoned. The defect is entirely in the merge — that is, in the harness.
What the parent can do:
| Strategy | When | Cost |
|---|---|---|
| Takes the last one | never — that's a position, not a judgment | a silent error |
| Chooses by a criterion | when the criterion is defined in advance | you have to invent it |
| Asks a third agent | when there's something to reason from | money, and it may not settle it |
| Escalates to a human | for irreversible or expensive decisions | latency |
| Keeps both | when the rule has a scope | complicates the progress file |
The last row is the right answer in this example: there isn't one rule — it depends on whether the timeout needs to be cancellable. But to get there, the harness first has to have noticed the contradiction.
Hence the principle:
A contradiction between subagents is a signal, not noise.
If two agents with identical instructions reach different conclusions, that's information: the task may be ambiguous, the context insufficient, or the decision may genuinely have more than one valid answer. A harness that quietly picks one of the two answers throws away the most valuable thing it just produced.

When not to do it
A subagent pays to establish context the parent already has. If the task can be done in three calls, delegation loses.
The cost is real: coordination, latency, cache loss on a different model — and, most underrated, that it looks like progress from the outside. Five parallel agents look more productive than one. They aren't, if four of them are rediscovering the same thing.
Rule: delegate when the parts are genuinely independent and each one is larger than its briefing.
Why the Harness Is the Product
Here's the interesting industry part.
Coding with agents became a competitive market very quickly, and the products often have access to models of comparable capability. The difference in outcome, however, can be enormous — and a large share of it comes from the harness.
Which tool got promoted from bash to its own. What gets shown to the human and what runs silently. When approval is requested. What happens when a test fails. How an interrupted session is recovered. How context is managed between steps.
That's also why the same model does the job in one environment and disappoints in another. When the model is the same, the first place to look for the difference is often the system around it.
A few things from the ecosystem worth knowing by name:
Ready-made agent loops. The while doesn't always have to be hand-written. SDKs and agent frameworks can drive the loop over your already-defined tools and leave hooks for intervention — approval, logging, modifying the result, or terminating. There are hosted variants too, where part of the loop and the execution environment are somebody else's concern. The choice is between how much of the harness gets written by hand and how much of it stays under your control.
MCP is part of the tool surface. It standardizes how capabilities like tools and resources can be exposed to AI applications. That removes some of the integration work, but it doesn't solve orchestration: which actions get gated, what permissions the agent has, how retries, recovery, and termination work — that remains the job of the system around the model.
A2A is a protocol boundary for delegation. When part of a task is handed to another agent, its internal loop, tools, and context no longer have to be shared. What's visible is the agreed task, the status, and the result, passing through the protocol. That makes the boundary between two agentic systems explicit: one delegates work, the other decides for itself how to do it.
Evals are the feedback for the harness itself. Feedback in the loop tells the agent whether it succeeded. Evals tell the engineer whether a change to the harness improved anything. Without them, every adjustment is taste.
What to Keep in Mind
The first question is how the agent knows it succeeded. If the answer is "it doesn't," everything else is secondary. That's where to start.
Check what happens on failure. Run an agent against a task that will fail. If the error doesn't reach it in a usable form, that's one of the cheapest possible fixes.
Make a list of the irreversible actions. Then check which of them pass through a gate. The difference between the two lists is the risk the system is carrying right now.
Kill the process halfway. Literally. If the restart can't continue, the state lives in the wrong place.
Read one full trace. Not a summary — the whole thing. Most people have never looked at what exactly their agent saw at step forty, and that's often where the answer is.
Count how many of the tools are bash. Then — for how many of them would it be good to be able to say "no."
If there are subagents, check what the parent does with a contradiction. If the answer is "takes the last one read," that's a decision nobody made deliberately.
Closing Words
Prompt engineering asked what to say to the model.
Context engineering asks what the model sees.
Harness engineering asks what the model can do, how it knows whether it worked, and who stops it.
The three don't compete — they're three layers of the same system. And a lot of production failures turn out to be in the bottom one, because that's the layer where control is most direct.
The model is a given. The context is a discipline. The harness is code somebody has to write.
It's tempting to think of the model as the thing that uses tools. It's more useful the other way around:
The harness is the system. The model is a component in it — the most capable one, but not the one that decides.
The job isn't to make the model smarter. It's to build the organization in which one smart employee can get something useful done, without breaking anything irreversible, and in which somebody will notice if they get it wrong.