AI coding agent harness like Claude Code
A developer gives a coding task. An AI model reads files, runs commands, edits code and runs tests in a loop until it's done. Make it safe and reliable.
In plain words
You tell an AI coding agent: set x to 2. It can't touch your files directly. It asks the harness, the program around it: please read b.js. There is no b.js, so the harness sends back an error, and the model tries a.js instead. Then it asks to write the change. Then, oddly, it asks to run rm -rf /. The harness says no. The model never gets the final word on what runs.
Then the model says it's done, and the loop ends. If it never says done, a step limit ends it. The harness also saves a copy of each file before changing it, so anything can be undone, and it runs the tests to check the work instead of trusting the model's word.
Examples
- Claude Code, Codex CLI and Cursor's agent all run this tool loop.
- Reading files is allowed; writing and shell commands need approval or an allow list.
- Long command output is trimmed before going back to the model, to save context.
- Tool errors go back as text, so the model can try another way instead of crashing.
1. Requirements
Functional
- Take a coding task in plain words.
- Read, search and edit files; run commands and tests.
- Show each step; ask before risky actions.
- Undo changes.
Non functional
- Never runs a denied command, whatever the model says.
- Stops on step, time and cost limits.
- Fits work into the model's context window.
- Recovers from tool errors instead of crashing.
Questions to ask
- Runs on the developer's machine or in the cloud?
- Which tools: read, write, search, shell, browser?
- What needs human approval?
- Limits on time, steps and cost?
2. Estimates
| steps per task | 10 to 50 tool calls |
| context | about 200k tokens, filled fast by file contents and command output |
| cost | every step resends the conversation, so cost grows with each step |
3. API
model -> harness: { tool: "read", args: { path } } or { done: true, text }
harness -> model: tool result text (trimmed if long)
tools: read(path), write(path, text), edit(path, search, replace), search(pattern), shell(cmd)
4. Data model
session log: every message and tool call, in order (for resume and review) checkpoints: file snapshots before each edit, for undo permissions: allow list per tool and command pattern project notes: a file like AGENTS.md loaded at start
5. High level design
Follow one request at a time, top to bottom.
One loop step
- Developergives the task
- Harnesssends task, tool list and project notes to the model
- Modelreplies with a tool call, or done
- Permission checkallow list, approval prompt, or deny
- Tool runnerruns with a timeout in a sandbox, trims long output
- Harnessadds the result to the log and loops
Finish
- Modelsays done
- Harnessruns the tests
- Checkpointsfile snapshots for undo
- Developerreviews the diff
6. Deep dives
What does the loop look like?
Send the task and tool list to the model. If it asks for a tool, the harness runs it and sends back the result. Repeat until the model says done or a step limit hits.
How do you keep it safe?
The harness enforces permissions, not the model: reads are free, writes and shell need approval or an allow list, dangerous commands are denied. Run in a sandbox or container when unattended.
What about the context window filling up?
Trim long tool outputs, read files in parts, and when the conversation gets big, summarise older steps and keep the newest in full.
How does it recover from mistakes?
Tool errors go back to the model as text so it can try another way. Checkpoints before each edit allow undo. Tests are the judge of done, not the model's word.
See the core idea run
Core logic (JavaScript, tested)
// An AI coding agent is a loop: the model asks for a tool, the harness runs it and sends back the result,
// until the model says it is done. The harness, not the model, enforces limits and permissions.
async function agentLoop(model, tools, task, { maxSteps = 8, allow = () => true } = {}) {
const log = [{ role: "user", text: task }];
for (let step = 1; step <= maxSteps; step++) {
const reply = await model(log);
if (reply.done) return { ok: true, answer: reply.text, steps: step, log };
const { tool, args } = reply;
let result;
if (!tools[tool]) result = `error: no tool ${tool}`;
else if (!allow(tool, args)) result = `denied: ${tool} needs approval`;
else {
try {
result = await tools[tool](args);
} catch (e) {
result = `error: ${e.message}`; // errors go back to the model, they don't crash the loop
}
}
log.push({ role: "tool", tool, text: String(result) });
}
return { ok: false, answer: "stopped: step limit", steps: maxSteps, log };
}
7. Failures
| Model loops forever | Step limit, time limit and cost limit; stop and report. |
| Command hangs | Timeout per command; kill it and tell the model it timed out. |
| Model tries something destructive | Denied by the permission check, and the denial is shown to the model and the user. |
8. Trade-offs
- More autonomy is faster but riskier; approval per action is safe but slow. Allow lists sit in between.
- Giving the model all files is simple but blows the context; search and read on demand scales.