Harness Engineering for Coding Agents
A harness is the loop, tools, tests, and permissions around a coding model. The model is the easy part. The harness is why some repos get useful diffs.
- agents
- coding-agents
- harness
Drop the same model into a chat box and you get an essay about your bug. Drop it into a loop that can list files, patch, and run tests, and you get a diff. That surrounding loop is the agent harness: tools, permissions, repo maps, a test runner, a sandbox, and a stop rule. The model is a sampler you can swap. The harness is the product.
Claude Code and Codex-shaped tools are harnesses with opinions (repo-aware file tools, permission prompts, CI). Shipping your own is the same job at a smaller scale: make the next action cheap, observable, and hard to do as root.
Key takeaways
- An agent harness is the loop plus tools, tests, and permissions around a coding model.
- Give the model a repo map and file tools. Do not paste the tree into the prompt.
- Sandbox the shell. Allowlist commands and paths. Prod credentials do not belong in that process.
- Retry on red tests with a budget. Stop if the same failure repeats.
- The diff and the test log are the artifacts. The model’s “it works” sentence is not.
What an agent harness contains
Think of a CI job that can think. The model proposes the next tool call. Your process runs it under policy. Observations go back. You cap steps. That is the whole architecture. If you skip the test runner, you built autocomplete with extra tokens. If you skip the allowlist, you built a junior with root.
| Piece | Job |
|---|---|
| Repo map | Paths, exports, test command. Cheap orientation. |
| File tools | Read, search, patch. Not “write entire file from memory.” |
| Shell | Sandboxed, allowlisted, with timeouts. |
| Test runner | The oracle. Red/green is data, not a vibe. |
| Permissions | Path allowlist, no .env, no git push --force. |
| Stop rules | Max steps, repeated test failure, human reject. |
Tool schema design matters here more than prompt poetry. A tool named run with a free-string command is how you get incident review. A tool named run_tests with no args is how you get a patch.
The loop you ship
type AgentEvent =
| { type: 'read'; path: string }
| { type: 'search'; query: string }
| { type: 'apply_patch'; path: string; diff: string }
| { type: 'run_tests' }
| { type: 'stop'; reason: string };
const ALLOWED_ROOTS = ['src/', 'tests/', 'package.json'];
const MAX_STEPS = 20;
const MAX_TEST_ROUNDS = 3;
export async function runHarness(goal: string, repo: RepoFs): Promise<string> {
const map = await repo.summarize({ maxFiles: 80 });
let trace = `Goal: ${goal}\nRepo map:\n${map}\n`;
let testFails = 0;
let lastFail = '';
for (let step = 0; step < MAX_STEPS; step++) {
const event = await model.nextEvent(trace);
if (event.type === 'stop') return event.reason;
if (event.type === 'read' || event.type === 'apply_patch') {
assertAllowed(event.path, ALLOWED_ROOTS);
}
if (event.type === 'apply_patch') {
await repo.applyPatch(event.path, event.diff);
trace += `patched ${event.path}\n`;
continue;
}
if (event.type === 'run_tests') {
const result = await repo.exec('npm test', { timeoutMs: 60_000, cwd: repo.root });
trace += `tests exit=${result.code}\n${tail(result.stdout, 4_000)}\n`;
if (result.code === 0) return 'tests green';
testFails += 1;
if (result.stdout === lastFail || testFails >= MAX_TEST_ROUNDS) {
return 'stopped: tests still red';
}
lastFail = result.stdout;
continue;
}
const observation = await repo.dispatch(event);
trace += `obs ${observation}\n`;
}
return 'stopped: max steps';
}
function assertAllowed(path: string, roots: string[]) {
const norm = path.replace(/^\.\//, '');
if (norm.includes('..') || norm.startsWith('/')) throw new Error('path');
if (!roots.some((r) => norm === r || norm.startsWith(r))) {
throw new Error(`blocked path ${path}`);
}
}
Failure modes the comments are for: path escape, infinite red-retry, stuffing 80k of test output into the next prompt. Tail the log. If you need the full log, write it to a file the model can read by offset.
Sandbox and permissions
The shell is the blast radius. Run it as a user who cannot reach production, with a network allowlist (package registry, nothing else) or no network. Do not mount cloud keys. Do not let apply_patch touch .env, id_rsa, or auth.json.
Frontend-specific: npm test is not enough if the agent can delete the snapshot folder and call that green. Pin the test command in the harness. The model should not choose npm test -- -u unless a human said so. Put that in code, not in a system-prompt plea.
CI is part of the same product. A patch that only “looks good” in the agent’s sandbox still goes through eval gates. The harness is the inner loop. CI is the outer loop.
Retry on red, then stop
A useful harness treats a failing test like a compiler: feed the error, try again, bound it.
- Run the targeted test, not the universe, after a small patch.
- If the error text is identical twice, stop. The model is looping.
- If the agent deletes tests, fail the run in the harness, not in code review two hours later. Diff the
tests/tree before you trust green. - Show the user the patch and the last test log. That pair is the PR.
Products like Claude Code ask before destructive edits. Copy that instinct even if you never use their CLI: file writes are a permissioned tool, not a default. Coding agents are what the user sees. The harness is why the diff is small.
Interview answer (out loud)
“An agent harness is the product around a coding model: repo map, file and test tools, a sandbox, path allowlists, and a max-step loop that retries on red tests then stops. I would not paste the repo into the prompt. I would not give a free-form shell. Green tests after deleting tests is a harness bug. The model is swappable. The loop is not.”
Further reading (primary sources)
Related on this site
Related guides
- Coding AgentsA coding agent reads, edits, and runs tests in a repo. Permissions, diffs, and why "it compiles" is not the acceptance test.
- Agent Failure ModesAgents loop, lie about tool results, skip confirmation, and get stuck apologizing. Catalog the failures and the circuit breakers.
- Agent HandoffsA handoff passes the conversation to another agent with a slimmer prompt and different tools. What to copy, what to drop, and how to avoid ping-pong.
- Agent MemoryAgents forget when the context window ends. Working memory, session summaries, and long-term stores are three different systems. Do not mix them.
- Agent System Design InterviewDesign a support or coding agent in a loop: tools, approvals, traces, evals, and cost. The frontend surfaces matter as much as the model.