# Unpacking the Harness Most people who use coding agents have no idea what they're actually using. They type into Claude Code or Codex CLI, watch it run commands, edit files, fix tests, and they form a mental model that goes something like: *the AI is doing things*. That mental model is wrong in ways that matter, and the wrongness compounds the more sophisticated your usage gets. If you don't understand the harness, you don't understand why your agent loops escape, why your token bills are what they are, why some tasks feel magical and others feel like watching someone fumble a Rubik's cube in the dark. This post is the one I keep meaning to write. Let's pull the casing off. --- ## The model is not the agent A model is a function. Tokens go in, a probability distribution over the next token comes out. You sample from that distribution, append the chosen token, and run the function again. That's it. There is no memory between calls. There is no "running" the model. There is no background process. Each forward pass is a fresh universe that ends the moment the last token drops. Everything else — every behavior you associate with an "agent" — is constructed by code that wraps that function. That wrapping is the harness. The harness is what holds the working memory. The harness is what turns structured output into actual filesystem operations. The harness is what decides when to stop, when to compact, when to spawn, when to checkpoint. The harness is the substrate on which the model performs. The reason this distinction matters: when people talk about "agent capabilities," they're almost always talking about harness capabilities. Claude doesn't read your repo. The harness reads your repo and shows fragments to Claude. Claude doesn't run your tests. The harness runs your tests and stuffs the output back into Claude's next forward pass. The intelligence and the agency live in different places. Conflating them is the single most common mistake in this entire field. --- ## A turn, fully unpacked Watch what happens when you type *fix the failing test* into Claude Code. The harness assembles a context window. This is a single string — sometimes fifty thousand tokens, sometimes half a million — that constitutes the entire universe the model is about to inhabit. Into it goes the system prompt (rules of behavior, formatting conventions, safety boilerplate), a developer message describing the environment (working directory, OS, shell, available tools, often git status), tool schemas in JSON, your message, and any attached file content the harness chose to inline. That blob is what the model sees. Nothing else exists. Not your other terminal windows, not the file you didn't attach, not the conversation you had yesterday. If it isn't in the context window, it is not in the world. The harness sends this blob to the inference endpoint. Tokens stream back. Most of them are prose — the model thinking out loud, narrating its plan. But interleaved with the prose are special structural markers the model has been trained to emit when it wants to *do* something rather than say something. The exact format varies — XML tags, JSON blocks, a custom DSL — but the function is identical: the model is signaling *stop generating, I have work to give you*. The harness watches the stream. The instant it sees the opening marker for a tool call, it stops accepting tokens, parses the structured payload, and dispatches. Dispatch is where the magic supposedly happens but is really just plumbing. The harness has a registry — a dictionary, basically — mapping tool names to handlers. When the model emits `bash` with input `pytest -x`, the harness looks up the bash handler and runs it. The handler spawns a subprocess, captures stdout and stderr, applies a timeout, truncates if the output is huge, and returns a result. That result becomes a new message in the context. Tagged as a tool result, attributed to the tool that produced it, formatted in whatever shape the harness uses for tool outputs. The harness now has a longer context than it started with — your message, the model's prose, the tool call, the result. It sends the whole thing back to the inference endpoint and asks for more tokens. The model, freshly instantiated again with no memory of the previous call, reads the entire transcript, sees its own earlier reasoning and the result that came back, and continues. Maybe it emits another tool call. Maybe it writes a final answer. Maybe it asks you a question. Whatever it does, the loop continues until the harness decides to stop. That decision — when to stop — is the harness's, not the model's. The model can signal "I'm done" with an end-of-turn token, but the harness is what honors it. The harness can also override: bail on excessive looping, kill on token budget, abort on a tripped guardrail, exit on user interrupt. There is no agent. There is a loop. --- ## How the agent senses bash Sensation in a harness is not what you think it is. The model has no eyes. It cannot poll your filesystem. It cannot tail logs in the background. It cannot keep watch on a process. The only sensation it has is what the harness chooses to splice into its next context window. When Claude Code "explores your repository," what's actually happening is the model is emitting `ls`, then waiting for the harness to inject the result, then emitting `rg "TODO"`, then waiting again, then emitting `cat src/main.py`, and so on. Each call is a single photon of perception. The model is groping in the dark with one finger, and between each touch, the entire universe blinks out and reconstitutes from the transcript. This explains a lot of behavior that otherwise looks puzzling. The reason agents over-explore is that they have no persistent spatial map of your codebase — every forward pass, they reread their own notes to remember what they've seen. The reason agents repeat tool calls they already made is that under context pressure they sometimes lose track of what they did. The reason agents miss obvious files is that *no one showed them the file*; the harness only inlines what was explicitly fetched. Bash is the most powerful tool in the harness because it's an escape hatch into the host's full capability. With bash, the model can do anything the harness's process can do — read, write, network, install, kill. This is also why bash is the most dangerous tool. A hostile string in a file the model reads can become an instruction the model executes. Prompt injection is not a bug in the model; it's a structural property of using a token predictor as a control plane for a shell. Good harnesses sandbox bash. They run it in an ephemeral container, restrict the network, deny writes outside a working directory, log everything. Codex CLI does this. Claude Code's sandboxing posture has evolved; the current default leans on user confirmation for dangerous operations. Both approaches are tradeoffs, and both leak. --- ## Tool discovery, calling, processing Tools don't appear by magic. The harness declares them, in a schema the model has been post-trained to read. When you start a session, the harness sends — as part of the system or developer prompt — a list of available tools, each with a name, a description, and a JSON schema for its inputs. The model uses these descriptions to decide which tool to call and how to fill in the arguments. The descriptions are *prompts in disguise*. A poorly written tool description will be misused; a well-written one teaches the model exactly when to reach for it. Discovery at runtime is a more interesting beast. Modern harnesses increasingly support deferred or lazy tool loading: instead of dumping every tool into the system prompt at session start (which costs tokens on every turn), the harness exposes a meta-tool — `search_tools` or equivalent — that the model can call to find capabilities on demand. This is how Claude's MCP integrations scale. With a thousand connectors available, you can't possibly list them all upfront. You let the model search. Calling a tool is a structured emission. The model writes the tool name and a JSON payload conforming to the input schema. The harness validates against the schema before dispatching. If validation fails — wrong types, missing fields, malformed JSON — the harness returns a structured error and the model retries. This validation step is doing more work than people realize: it's catching a meaningful fraction of model mistakes before they become production incidents. Processing the output is where harnesses differ most. Naive harnesses just stuff stdout into the context. Sophisticated harnesses do real work: - *Truncation strategies*: head, tail, head+tail, semantic summarization. A 200KB log file is going to blow your context window and cost you ten cents in tokens. Pick a policy. - *Structured extraction*: parsing JSON outputs, exposing them to the model as objects rather than strings. - *Caching*: if the model ran `cat config.toml` three turns ago and nothing has changed, the harness can serve the cached result and save a roundtrip. - *Redaction*: stripping secrets before they enter context. This is the layer where most production engineering happens. Models are commodity. Harness behavior is where the differentiation lives. --- ## The other primitives Past the loop and the tools, the harness has a stable of supporting machinery. **Context management.** The single most important and least appreciated harness primitive. As a session grows, the context grows linearly, and inference cost grows superlinearly with context length on most architectures. Naive harnesses just keep appending. Real harnesses compact. They summarize earlier turns, drop superseded tool outputs, prune redundant file reads, and reconstruct a working transcript that fits the budget. Claude Code does this aggressively. Codex does this more conservatively. The choice has user-visible consequences: aggressive compaction means longer sessions but occasional amnesia; conservative compaction means crisper recall but a wall you eventually hit. **Working memory artifacts.** Harnesses increasingly let the model write to a side channel — scratchpad files, plan documents, todo lists — that survive across turns without consuming context on every call. The harness shows the model a pointer ("you have a TODO file at .agent/todo.md") and the model can read or write it via tools. This is the harness building external memory because the model has none. **Permissioning and guardrails.** A function that asks "is this safe to run?" before bash executes. A function that diffs proposed file edits and shows them to you before write. A function that blocks `rm -rf` patterns. These are policy code, not model code. They live in the harness. **Token accounting and budgets.** The harness tracks input tokens, output tokens, cached tokens, tool call counts. It enforces budgets — per turn, per task, per session. When the budget tilts, the harness can warn, compact, or terminate. **Structured output coercion.** When the harness needs JSON back from a tool result, it doesn't trust the model to produce valid JSON; it constrains decoding so the model literally cannot emit invalid syntax. This is a harness capability, leaning on the inference endpoint's grammar/schema features. **Streaming and interruption.** The user being able to hit ctrl-c, the harness gracefully cutting the stream, the model getting a synthetic "user interrupted you" message on resume — that's all harness work. **Logging and replay.** Every serious harness records the full transcript, every tool call, every result. This is what makes debugging possible. It's also what makes deterministic replay possible — you can take a recorded session and re-run it against a different model to see how behavior changes. This is the foundation of any real evaluation infrastructure. --- ## A real session, end to end Abstract is fine. Concrete is better. Let's walk through a session — the kind I run several times a week — and trace what's actually happening underneath. The setup: a Rust service in a git repo, deployed via GitHub Actions to a staging environment. The user types into Claude Code: > *"The /healthz endpoint started returning 500s in staging this morning. Find the regression, fix it, get it deployed, and watch the rollout."* Watch what unfolds. **Turn 1 — Orientation.** The harness assembles the opening context: system prompt, tool schemas (bash, read, edit, web_fetch, plus whatever MCP tools are connected — say, GitHub and the CI provider), the working directory tree, git status, the user's message. Maybe forty thousand tokens before the model has even started. The model emits a short plan in prose — *I'll start by reading the recent commits, then look at the healthz handler, then check CI* — and immediately calls `git log --oneline -20`. The harness runs it. Twenty lines come back. Injected into context. Model continues. **Turn 2 — Hypothesis formation.** The model spots a commit from this morning touching the health module and calls `git show `. Diff comes back. About 800 lines, because the commit also did some refactoring. Now the harness has a decision: dump all 800 lines verbatim, or summarize? Naive harnesses dump. Claude Code dumps with a soft cap and lets the model ask for more if needed. The model reads the diff and forms a hypothesis: a database connection pool change looks suspicious because the new code holds a lock during the readiness probe. **Turn 3 — Verification.** Model calls `read` on `src/health.rs` to see the current state. Then `rg "lock"` to find related call sites. Each of these is a separate forward pass: emit the tool call, harness dispatches, result injected, model regenerates the *entire* working transcript in its head before producing the next token. The model has no continuity between turns except what's in the context window. Every turn it rereads its own notes to remember where it is. This is the part that surprises people. The model isn't "thinking through the problem" the way a human engineer does, holding state in working memory across hours. It's reconstructing its understanding from the transcript, every single turn. The transcript *is* the working memory. Lose the transcript, lose the agent. **Turn 4 — Reproduction.** Model writes a test that exercises the suspected race, runs `cargo test health::concurrent_probes`. Test fails the way the model predicted. Now the harness has captured stdout, stderr, and the test framework's output — about 3KB. Injected. Model is now confident. **Turn 5 — The fix.** Model calls `edit` on `src/health.rs`, replacing the lock acquisition with a try-lock pattern. The harness shows the diff to the user, gets confirmation (or not, depending on permission settings), and writes the file. Then the model reruns the test. Pass. Reruns the broader test suite. Pass. About 8KB of test output now sits in the context. **Turn 6 — Commit and push.** Model calls bash with `git add -p` equivalents, then `git commit -m "fix: avoid lock contention in health probe"`, then `git push origin fix/health-lock`. Three subprocess invocations, three result injections. Then it calls the GitHub MCP tool to open a PR — `github.create_pull_request` with title, body, base, head. The harness dispatches to the GitHub MCP server. JSON comes back with the PR URL and number. Injected. **Turn 7 — CI watch.** This is where it gets interesting. The model wants to watch CI. It can't actually *watch* anything — there is no event loop, no subscription. So it polls. It calls `github.get_pr_checks` with the PR number. The harness hits the GitHub API, gets a JSON blob of check runs, returns it. Maybe 5KB of JSON. Model sees that builds are queued. It cannot just sit and wait — every "wait" is another forward pass that costs tokens. So the harness offers a `sleep` tool, the model calls `sleep(60)`, the harness actually sleeps for sixty seconds and then returns control. Model polls again. Still running. Sleep again. Polls again. Now CI is green. You're now thirty thousand tokens deeper than where you started. Most of those tokens are CI JSON blobs that have been superseded by newer ones, plus tool result echoes, plus the model's running narration. **Turn 8 — Deploy.** Model calls the CI provider's MCP tool to trigger the staging deploy workflow — `ci.run_workflow` with the workflow name and ref. JSON comes back with a run ID. Model polls the run. Waits. Polls. Run completes. Deploy succeeds. **Turn 9 — Verification in staging.** Model calls `web_fetch` against `https://staging.example.com/healthz`. Harness fetches, returns 200 OK with a small JSON body. Model reports back to the user: *fixed, deployed, healthz green.* That's the happy path. Forty turns is plausible for this kind of session. Two hundred thousand input tokens consumed across the run, because remember — every turn replays the entire transcript on input. The output tokens are small. The input tokens are where the bill lives. --- ## Where this session breaks Now run that same session a few times in production and watch what actually goes wrong. **Compaction collision.** Around turn twenty, the harness notices the context is getting heavy and runs a compaction pass. It summarizes turns 1–10 into a paragraph: *"Investigated PR #847 in src/health.rs, identified lock contention in the readiness probe, wrote a reproduction test that confirmed the race, fixed with try-lock pattern, all tests pass."* That summary is correct but lossy. The original turns contained the exact line numbers, the exact diff, the exact test output. The compacted version contains the *gist*. Now in turn 28, the model needs to write a postmortem comment on the PR and wants to cite the specific lock acquisition that was at fault. It can't. The detail was compacted away. So it makes one up — confidently, plausibly, wrong. This is the hallucination-via-summarization failure mode and it is everywhere in long sessions. The model isn't lying; the model is reconstructing from a lossy snapshot and filling in the gaps with its prior. The harness made the compaction call for good reason — staying under token budget — but the cost was paid downstream, in a turn where the harness was no longer watching. The mitigation that works: don't compact source-of-truth artifacts. The harness should preserve, verbatim, things the model is likely to need to cite — diffs, test outputs, error messages — and compact only the conversational narration around them. Most production harnesses are still figuring this out. Aggressive compaction looks great in benchmarks that test breadth and looks terrible in workflows that require precision. **The CI poll loop blowup.** Suppose the deploy is slow and CI takes twenty minutes. The model is polling every sixty seconds, and each poll returns 5KB of JSON. After twenty polls, you've added 100KB of nearly-identical CI status blobs to the context, each one differing by a timestamp and a percent-complete. The model is now spending forward passes reading stale poll results to remember what it's already seen. A good harness deduplicates. It notices that the third, fourth, and fifth poll returned essentially the same payload as the second, and it elides the redundant ones, replacing them with `(3 identical polls elided)`. This is harness intelligence, not model intelligence. The model can't see what the harness chose not to show it — and it shouldn't need to. **The handoff failure.** The GitHub MCP tool returned a PR object. The CI MCP tool wants a workflow ref. The model has to bridge the two — it has to take a field from one tool's output and pass it as input to another. This is where structured tool outputs matter enormously. If the GitHub tool returns `{"head": {"ref": "fix/health-lock", "sha": "abc123..."}}`, the model can pluck the field. If it returned a prose blob saying *"PR opened on branch fix/health-lock at commit abc123,"* the model has to *parse* that prose, and parsing is where models hallucinate. The handoff between agents and tools is fundamentally a serialization problem. The cleaner the schemas, the more reliable the chain. Harnesses that expose tools with sloppy outputs end up with agents that look unreliable, when really the unreliability lives in the seams. **Hitting the wall.** Sometimes the session just runs out of room. You're at 180K tokens of a 200K window. Compaction has already run twice. There's no more juice to squeeze. The harness has to make a hard call: terminate the session and report what's done, or aggressively prune even load-bearing context and risk the model losing the plot. Claude Code's behavior here is to warn the user and offer a fresh session that inherits a summary plus the working directory state. Codex tends to be more conservative about how full it lets the context get in the first place. Both approaches have trade-offs. The warning approach trusts the user to make the call. The conservative approach saves you from yourself but caps how much you can do in a single session. The honest thing to admit is that the context window is the real ceiling on agent autonomy right now, far more than model capability. A sufficiently smart model with a sufficiently small context will fail at long-horizon work. A modestly smart model with great harness-level memory management will outperform it. This is the asymmetry that makes harness engineering valuable. **The destructive action that wasn't reversible.** Somewhere in the middle of all this, the model called `git push --force` because it rebased the branch. The harness allowed it. The push wiped a colleague's commit that had been pushed to the same branch five minutes earlier. The model didn't know. The harness didn't check. This is the class of failure that most haunts production agent deployments. The model reasoned correctly given the information it had. The information it had was incomplete in a way that the harness was responsible for closing. Good harnesses run pre-flight checks before destructive git operations — fetch first, look for divergence, refuse force-push if the remote has new commits the local branch doesn't know about. Bad harnesses just run what the model emits. The general principle: every irreversible operation deserves a harness-level guard, because the model has no way of knowing what it doesn't know. The cost of the guard is one extra subprocess call. The cost of skipping it is sometimes a postmortem. --- ## Multiple agents Now multiply. A multi-agent system is, structurally, just a harness that knows how to spawn other harnesses. The pattern: the top-level model emits a tool call called something like `dispatch_subagent` with a goal description. The harness, instead of running bash, spins up a *new* harness instance with its own context window, its own tools, its own loop. The subagent runs to completion, returns a summary, and that summary is injected back into the parent's context as the result of the dispatch call. From the parent's perspective, calling a subagent is indistinguishable from calling bash. Same loop, same dispatch, same result injection. The recursion is happening one level down where the parent can't see it. Why bother? Because context is the binding constraint. A subagent operates in its own context window — it can read fifty files, run twenty searches, burn three hundred thousand tokens — and the parent only ever sees the final summary. You're trading a complete record for a compressed one, and that trade buys you breadth that no single-agent loop can achieve. The cost is faithfulness. A summary is lossy. The parent can't audit what the subagent actually saw. If the subagent hallucinated a finding, the parent inherits the hallucination as fact. This is why production multi-agent systems lean heavily on structured handoffs — the subagent doesn't return prose, it returns a typed object the parent can introspect. Coordination patterns that show up in practice: - *Orchestrator-worker*: one planning model dispatching many specialist subagents in parallel. Anthropic's research agent uses this. It's good for breadth-first work — search, comparison, fan-out. - *Pipeline*: subagents chained in sequence, each consuming the previous one's output. Good for transforms with clear stages. - *Debate*: multiple agents on the same problem, with a judge agent reconciling. Good for tasks where verification is easier than generation. - *Hierarchical*: subagents that themselves spawn subagents. Powerful, fragile, expensive, hard to debug. Most teams that try this scale it back. The practical lesson from running these in production: every layer of indirection costs you faithfulness, latency, and money. Two layers is usually the right number. Three is for people who have measured. Four is a smell. --- ## Checkpoints Checkpointing is the harness primitive that separates toys from tools. A checkpoint is a serialized snapshot of harness state — the context window, the tool registry, the working directory, any open file handles or background processes — that can be restored later, possibly on a different machine, possibly by a different user. Why does this matter? Because long-horizon agent work is high-variance. The model goes down a wrong path. Bash trashes a file. A subagent burns the budget on a dead end. Without checkpoints, your only recovery is to start over. With checkpoints, you fork from the last known good state, try a different branch, and keep what works. This is also how you get reproducibility. A checkpoint is a recipe. Same starting state plus same inputs plus same model equals same outputs — modulo sampling temperature, which a serious harness pins to zero or seeds explicitly. Implementation reality is uglier than the concept. Context windows serialize cleanly. Filesystem state serializes via copy-on-write or snapshotting. Subprocess state mostly doesn't serialize at all — you kill it and hope. Network state, even less so. So most checkpoints are partial; they capture the model-visible state and accept that side effects on the outside world have already happened. The interesting design question is *when to checkpoint*. Every turn is too expensive. Only at user request is too rare. The right answer is event-driven: checkpoint before any destructive action, after any expensive computation, at natural task boundaries. This is policy that lives in the harness, and it's where harness designers earn their keep. Checkpoints also unlock time travel as a debugging primitive. When something goes wrong, you don't just look at logs — you fork from the checkpoint just before the failure and try alternate paths. This turns post-mortems from forensic exercises into actual experiments. --- ## How the major harnesses differ The models — Claude, GPT-5.5, Gemini — are increasingly converging in raw capability. The harnesses are where the actual product differentiation lives, and they have radically different philosophies. **Claude Code** is a thick, opinionated harness. It assumes you want it to take initiative. It compacts aggressively, invokes tools speculatively, manages long sessions with a heavy hand. It is built around the assumption that the model is good enough to trust with broad latitude, and that the harness's job is to extend that latitude as far as safety allows. The MCP integration story is the deepest in the field — by standardizing tool surfaces, Anthropic effectively turned the harness into a platform. **Codex CLI** is leaner and more deterministic. The harness is more conservative about what it does autonomously, more explicit about confirmations, more sandboxed by default. The recent /goal addition is interesting — it's effectively a built-in Ralph loop, a policy that says "keep going until the goal is met or the budget is exhausted." Codex tends to reflect OpenAI's house style of releasing primitives and trusting power users to compose them, where Claude Code reflects the house style of pre-composing things and shipping the composition. **Cursor and Windsurf and the IDE-embedded harnesses** are doing something different again — they're harnesses where the IDE itself is the tool surface. The model's "sensation" includes things like cursor position, current selection, file tree, language server diagnostics. The loop is the same; the perceptual richness is dramatically higher. The cost is portability — you can't take that harness anywhere except the IDE. **Aider, Continue, the open-source harnesses** are where the design space is most legible because the code is open. If you want to actually understand harness mechanics, read Aider's source for an evening. You'll never look at a closed-source agent the same way. The convergence I expect over the next year: every harness will have to expose its primitives more cleanly. Right now, most harnesses are black boxes — you get behavior, not levers. As people build more sophisticated workflows on top of agents, they'll demand control over context management, checkpointing, subagent dispatch, tool registries. The harnesses that expose these as first-class APIs will be the ones serious teams build on. The ones that hide them behind clever defaults will be the ones tinkerers love and engineers eventually outgrow. --- ## What this means If you take one thing from this post, take this: the harness is the product. The model is the engine, but the harness is the car. People who optimize their prompts without thinking about their harness are tuning the engine while ignoring the transmission. People who pick agent products based on which model is underneath are buying cars based on horsepower numbers without driving them. The interesting engineering — the stuff that determines whether an agent actually works in production — happens in the loop, the dispatcher, the context manager, the checkpoint policy, the subagent coordinator. This is also why I think the next wave of infrastructure value accrues to the layer above the model and below the application. The model providers have the weights. The application builders have the users. The harness is where the leverage is, and right now it's mostly being built ad hoc, in-process, undocumented, untested. That gap is going to close, and the people who close it will own a piece of every agent that matters. The agent revolution isn't a model story. It's a harness story. The sooner you internalize that, the better your bets get.