I Couldn't Explain What a Harness Was in an Interview. So I Built One.

Last Friday, I was interviewing with company X.
It was going well. We were talking agents, spec-first development, velocity. My home turf.
Then the question landed:
“So, what exactly is a harness?”
Blank.
I mumbled something about “the environment around the model.” Vague. Soft. Not me.
The worst part? I use harnesses every single day. My agents run overnight, manage my newsletter, watch my commits, merge my PRs after review. I live inside these things.
But using something and being able to explain it are two different skills. That day, the second one failed me.
So the same afternoon, I did the one thing that always clears my head: I opened an empty file and built one. 200 lines of TypeScript. By the evening, I could explain a harness to anyone.
This article is the answer I should have given. With the code.
What a harness actually is
An LLM on its own does nothing. Text in, text out. That’s it.
It doesn’t read your files. It doesn’t run your tests. It remembers nothing between two calls.
The harness is everything you wrap around the model to turn that text generator into an agent that acts.
Four components. That’s all.
1. The loop
The core. A while loop that calls the model, executes whatever it asks for, feeds the result back, and goes again.
As long as the model wants to act, the loop spins.
When it’s done, the loop stops.
2. The tools
The agent’s hands. Functions you write — read a file, run a command, search the web — declared to the model. The model never executes them itself. It asks. The harness executes.
3. The memory
The message list. Every turn appends what the model said and what the tools answered. That’s an agent’s “memory”: an array that grows. Nothing magical.
4. The guardrails
What keeps the loop from going off the rails. Max turns. Budget. Forbidden tools. Timeouts. Without them, you don’t have an agent. You have an API bill stuck in an infinite loop.
That’s it. Loop, tools, memory, guardrails. Everything else — subagents, skills, sandboxes — is refinement stacked on top of those four bricks.
The 200 lines that matter
Here is the harness I built that Friday afternoon. Simplified for reading, but this is the real structure.
Start with the memory. A plain array of messages:
type Message =
| { role: "user"; content: string }
| { role: "assistant"; content: string; toolCalls?: ToolCall[] }
| { role: "tool"; toolCallId: string; content: string };
interface ToolCall {
id: string;
name: string;
input: Record<string, unknown>;
}
Then the tools. A registry of functions, each with a description so the model knows what it can call:
const tools = {
read_file: {
description: "Read a file and return its content",
run: async (input: { path: string }) =>
fs.readFile(input.path, "utf8"),
},
list_dir: {
description: "List the files in a directory",
run: async (input: { path: string }) =>
(await fs.readdir(input.path)).join("\n"),
},
run_command: {
description: "Run a shell command and return stdout",
run: async (input: { cmd: string }) =>
execSync(input.cmd, { timeout: 30_000 }).toString(),
},
};
Three tools are enough for an agent that explores a repo and runs tests. You add more as needed. That’s the whole point: the tools define what your agent can do — and therefore what it cannot do.
Now the dispatch. The model requests a tool, the harness runs it — and crucially, it sends errors back to the model instead of crashing:
async function dispatch(call: ToolCall): Promise<string> {
const tool = tools[call.name as keyof typeof tools];
if (!tool) return `Error: unknown tool "${call.name}"`;
try {
return await tool.run(call.input as never);
} catch (err) {
// The error goes back into the context: the model
// reads it and fixes its next attempt on its own.
return `Error: ${String(err)}`;
}
}
That catch taught me more than anything else.
A robust agent is not an agent that never fails.
It’s an agent that sees its failures and tries a different way.
And finally, the loop. The true heart of the harness:
const MAX_TURNS = 20; // guardrail: never an infinite loop
async function runAgent(task: string): Promise<string> {
const messages: Message[] = [{ role: "user", content: task }];
for (let turn = 0; turn < MAX_TURNS; turn++) {
// 1. Give the model the FULL history
const response = await llm.chat({
system: SYSTEM_PROMPT,
messages,
tools: toolSchemas(tools),
});
// 2. Memory accumulates
messages.push({
role: "assistant",
content: response.text,
toolCalls: response.toolCalls,
});
// 3. Stop condition: no tool calls = the work is done
if (response.toolCalls.length === 0) {
return response.text;
}
// 4. Otherwise: execute, feed back, loop again
for (const call of response.toolCalls) {
const result = await dispatch(call);
messages.push({
role: "tool",
toolCallId: call.id,
content: result,
});
}
}
return "Stopped: turn limit reached.";
}
Read that loop again. It fits in one sentence:
call the model → if it requests tools, run them and feed the results back → otherwise, done.
The first time I watched that for loop spin — the agent listing my repo, reading three files, running the tests, fixing, re-running — I understood why I had fumbled the interview.
I assumed “harness” hid some complicated concept. In reality, everything I use daily — Claude Code, my crons, my agent Hermes — is this exact loop, with better tools, better context management, and more guardrails.
The model brings the intelligence. The harness brings the structure. And you are the one writing the structure.
That is exactly how I see AI: an accelerator, not a replacement. The model doesn’t decide which tools exist, where the limits are, or what “done” means. That’s your job. And that’s where your years of experience actually count.
What I’d tell my interview self
If Friday’s interview replayed itself, here is my two-sentence answer:
“A harness is the loop wrapped around the LLM: it presents tools, executes the model’s tool calls, feeds the results back into the context, and repeats until the model returns finished work. The model provides the reasoning; the harness provides the hands, the memory, and the limits.”
Two sentences. Four components. One afternoon to earn them.
The real lesson isn’t technical. Fumbling a question happens. What matters is the delay between “I don’t know” and “now I know” — and proving it with code instead of an excuse.
Friday morning, I couldn’t explain a harness. Friday evening, I had one running.
This kind of topic — agents, harnesses, AI wired into real code — is literally what I do on freelance missions. If your team is building this right now, we can talk for 30 minutes: calendly.com/yelkamel/30min.