I’m currently working on iterations of our agent system and I wanted to research how other products are tackling similar changes. I studied eleven agent products, reading their source and what is known about them: how each one runs its loop, calls tools, manages context and memory, gates permissions, and splits into processes.
I wrote another article, “What is an agent harness?”, covering what a harness is and what to know when building one; this piece is about the decisions and state of different projects.
Coding agents dominate here because that is where the category got popular and where the code is most readable.
Claude Code: Anthropic’s terminal coding agent, and the closed reference point here; everything about its internals is reconstructed from the shipped bundle and a source map Anthropic shipped by accident in March 2026, since taken down.
OpenCode: a terminal coding agent built as a local server with the TUI as one client; the reference for running an agent anywhere other than a terminal.
Pi: Mario Zechner’s terminal coding agent; the minimal core, four tools and under 1,000 tokens of scaffolding.
Cline: a coding agent that lives inside the editor as an extension; the cleanest separation of a stateless loop from a stateful runtime.
Goose: Block’s open-source agent, now at the Linux Foundation; MCP as its only extension boundary.
Crush: Charmbracelet’s terminal coding agent; a permission model that hides denied tools from the model instead of blocking the call.
Gemini CLI: Google’s terminal coding agent; the widest sandboxing menu, with readable source.
Aider: a terminal pair programmer that commits straight to your git history; five edit formats picked per model, and the data behind them.
OpenHarness: HKUDS’s Python port of Claude Code; what a port keeps and drops.
OpenClaw: a personal assistant that meets you in your chat apps, not a coding agent; the most developed memory system read here.
oh-my-pi: a hard fork of Pi that became a rewrite; the most edit-format measurement in the set.
The agent loop in source: Pi, Claude Code and OpenHarness
Pi, from Mario Zechner, is the minimal version: stream an assistant response, execute the requested tools, append the results, emit turn events, continue. There is no max-step knob. The loop runs until the agent says it is done.
Pi’s loop is the plain agent loop (call the model, run the tools it asks for, append the results, repeat) with streaming, events and abort handling added. The source is packages/agent/src/agent-loop.ts, 796 lines at v0.84.3. The loop proper is runLoop, about 120 of them; the rest is streaming, tool execution and event plumbing. Here is the control flow, with cuts marked:
// Outer loop: continues when queued follow-up messages arrive after agent would stop
while (true) {
let hasMoreToolCalls = true;
// Inner loop: process tool calls and steering messages
while (hasMoreToolCalls || pendingMessages.length > 0) {
// ...
// Stream assistant response
const message = await streamAssistantResponse(currentContext, config, signal, emit, streamFunction);
newMessages.push(message);
if (message.stopReason === "error" || message.stopReason === "aborted") {
await emit({ type: "turn_end", message, toolResults: [] });
await emit({ type: "agent_end", messages: newMessages });
return;
}
// Check for tool calls
const toolCalls = message.content.filter((c) => c.type === "toolCall");
const toolResults: ToolResultMessage[] = [];
hasMoreToolCalls = false;
if (toolCalls.length > 0) {
// A "length" stop means the output was cut off by the token limit, so
// every tool call in the message may carry truncated arguments. Fail
// them all instead of executing potentially borked calls.
const executedToolBatch =
message.stopReason === "length"
? await failToolCallsFromTruncatedMessage(toolCalls, emit)
: await executeToolCalls(currentContext, message, config, signal, emit);
toolResults.push(...executedToolBatch.messages);
hasMoreToolCalls = !executedToolBatch.terminate;
for (const result of toolResults) {
currentContext.messages.push(result);
newMessages.push(result);
}
}
await emit({ type: "turn_end", message, toolResults });
// ...
if (
await config.shouldStopAfterTurn?.({
message,
toolResults,
context: currentContext,
newMessages,
})
) {
await emit({ type: "agent_end", messages: newMessages });
return;
}
pendingMessages = (await config.getSteeringMessages?.()) || [];
}
// Agent would stop here. Check for follow-up messages.
const followUpMessages = (await config.getFollowUpMessages?.()) || [];
if (followUpMessages.length > 0) {
// Set as pending so inner loop processes them
pendingMessages = followUpMessages;
continue;
}
// No more messages, exit
break;
}
Pi’s loop is the simplest of the eleven, so it is the baseline here; Claude Code and OpenHarness differ from it by what they add or drop at each step.
Tool execution: Pi runs a batch of tool calls in parallel by default, with a per-tool executionMode: "sequential" opt-out, and ends the run early only if every result in the batch sets terminate. Claude Code schedules by whether a call reads or writes, running reads concurrently and serializing writes. OpenHarness fires every tool in a batch at once with no read/write distinction, so two edits to the same file race.
Context: Pi exposes two hooks. transformContext rewrites the message list before each model call, and the doc comment’s example prunes old messages, which is where compaction plugs in; shouldStopAfterTurn ends the run after the current turn, “e.g. before context gets too full.” Claude Code decides whether to compact before every model call, retries when the context overflows, and assembles a system prompt that varies with over a hundred conditions. OpenHarness ports Claude Code’s compaction, covered below.
Permissions and repair: Pi’s beforeToolCall runs after argument validation and can block the call; a permission gate, if you write one, lives here, and containment is left to the container. Claude Code runs hooks and a permission check around each call, repairs the transcript when a call is interrupted, and retries when the response is truncated. OpenHarness keeps Claude Code’s shape and drops most of the recovery.
Input mid-run: Pi injects messages the user typed while the agent was working before the next model call, and follow-ups queued after the agent would have stopped restart the outer loop.
The difference between the three is how much the loop is expected to survive. Pi handles truncation and aborts in the loop and leaves permissions and containment to extensions and the container. Claude Code assumes the model will truncate mid-call, overflow the window, repeat itself, and reach for something that needs approval, and spends most of each iteration on those cases.
How a loop decides to stop
Every model response carries a stop reason, the provider’s one-word account of why generation ended. The three that matter here: the model finished (end_turn in Anthropic’s API), the model asked for tools (tool_use), or the output hit its token limit (max_tokens). Pi normalizes these across providers into its own names, which is why its code says length where the Anthropic API says max_tokens. The obvious loop continues while the stop reason is tool_use and exits otherwise.
None of the three production loops here does that. Claude Code sets needsFollowUp = true when it observes a tool_use block in the response content, regardless of the stop reason. OpenHarness’s README describes a loop that branches on response.stop_reason != "tool_use", but its code captures stop_reason from all four provider clients and ignores it; continuation runs off a structural filter for tool-use blocks. Pi’s message.content.filter((c) => c.type === "toolCall") decides the same way. Loop control in production is empirical: the content says what the model wants, and the stop reason is treated as a hint.
Ignoring the stop reason has a cost. A max_tokens truncation mid-tool-call leaves a partial tool call in the content, and if content is the only signal it looks identical to a clean finish. Pi’s fix is to read the stop reason for failure while ignoring it for continuation: error and aborted end the run, and length fails every tool call in that message with a result telling the model to re-issue it. OpenHarness, which never reads the field, cannot do the same.
A harness also has its own reasons to stop that the model knows nothing about: a turn cap, a context window that can no longer fit the conversation, a hook that says stop, a user who pressed escape. Pi has none of these in the loop beyond shouldStopAfterTurn; it runs until the model stops calling tools. Claude Code has a ten-state termination union that includes completed, max_turns, prompt_too_long, and hook_stopped. OpenCode’s while (true) exits only when four conditions hold at once: terminal finish reason, finish reason not tool-calls or unknown, no pending local tool calls, and the assistant message belongs to the current user turn. OpenCode’s processor returns "continue", "compact", or "stop", making compaction a loop outcome rather than a side effect; most harnesses run compaction as an imperative preamble at the top of each turn and hope.
Doom loops
A doom loop is the agent issuing the same tool call with the same arguments, getting the same result, and issuing it again, with nothing in the loop to notice. The usual cause is a tool result that says “try again” about a situation that will never change, which is the shape of both retry loops Arize found in its Alyx agent. Without a check, the loop runs until the turn cap or the budget ends it.
The detection is a counter over recent tool calls. OpenCode trips a doom_loop permission at 3 identical consecutive tool calls. Crush examines a 10-step window and triggers after more than 5 repeated tool-call signatures. Pi and OpenHarness have none; OpenHarness, with its 200-turn default, will spend two hundred turns re-reading one file.
Autonomous vs guided loops: where control returns to the person
Every loop here has two layers.
Agent inner Layer
The inner layer is what the harness decides on its own each iteration: whether to continue, compact or stop, which tool calls run and in what order, whether a call needs approval, whether the model is repeating itself.
Agent outer Layer
The outer layer is where the person gets back in: an approval prompt, a message typed mid-run, an escalation the harness raises, a session boundary. Pi and oh-my-pi have the shape literally, a nested while(true) where the inner loop runs tool calls and the outer loop drains messages queued while the agent worked; in oh-my-pi’s words, “the agent stopped” and “the agent is done” are different states.
Steering
Two channels in: Pi separates a steering message, delivered after the current turn’s tool calls finish, from a follow-up message, delivered only after the agent finishes all its work (Enter and Alt+Enter). Claude Code makes the same split at the abort layer: its AbortController hierarchy distinguishes a hard abort (Ctrl+C) from a submit-interrupt (the user typed a new message), and only the first tears the turn down.
Escalation thresholds: a classifier-gated loop still hands back to the person when it keeps saying no. Claude Code’s auto mode stops and escalates after 3 consecutive denials or 20 total. Codex’s auto-review trips after 3 consecutive denials or 10 within the last 50 reviews, and an approval never widens the sandbox or the network.
Approval placement: the permission mode is where the line between the two layers gets drawn. Goose defaults to fully autonomous, with manual, risk-based and chat-only modes beside it; Gemini CLI’s policy engine allows reads and asks on writes; Crush asks before every call by default; OpenClaw routes approvals to Slack DMs and iOS push and tells its reviewer that “’ask’ should be high fidelity, only ‘ask’ when you are genuinely unsure.”
Pause and resume: in a product rather than a terminal, the outer loop is a request boundary. The Claude Agent SDK exposes interrupt(), setPermissionMode() and a defer decision for approvals answered later; the OpenAI Agents SDK serializes a RunState to pause across HTTP requests, carry approvals and resume; the Vercel AI SDK takes a needsApproval predicate answered client-side; LangGraph’s interrupt() pauses through an exception and resumes with Command(resume=...); MCP’s elicitation lets a server ask the user for input mid-call. Session boundaries are the coarsest version: Anthropic’s initializer agent handing to an incremental coding agent, and Amp’s Handoff.
Auto-mode or ask first?
The approval numbers come mostly from vendors measuring their own products. Anthropic has put the share of Claude Code permission prompts users approve at 93% and, in a later post, 97%. In Anthropic’s own experiment with 1,053 paid professional testers, human review caught a dangerous command 13.6% of the time, falling from about 17% early in a session to about 5% after 50 prompts, while the auto-mode classifier caught 89%. OpenAI reports that Codex auto-review stops for a person about 200 times less often than manual approval.
The peer-reviewed counterpoint comes from medicine: in a simulated ICU with 38 physicians, clinicians rejected 92% of unsafe AI recommendations and also 29% of safe ones.
The reading for a harness: approvals are for consequential, batched, previewable decisions, and the boundary is the scope granted.
Component by component: what the source shows
Prompt caching and render order
A request renders in a fixed order: tools, then system, then messages. Tools come first. So any byte you change in the tool set invalidates the cache for the system prompt and for every message behind it.
Harnesses therefore ship a stable tool superset in a deterministic order instead of varying tools per task. Swapping the tool list to “focus” the agent looks like context engineering and is a cache bill.
The shipped alternative is deferred loading. Mark tools defer_loading: true and give the model a search tool to pull them in. Measured on a 50+ tool set: roughly 77K tokens down to 8.7K, an 85% reduction, with accuracy on Opus 4 moving from 49% to 74%. Two constraints: you cannot defer everything, and you cannot defer the search tool itself. Claude Code’s ToolSearch is this, on by default.
System prompt and instructions
Three channels in every request: a cached system array, the tools as JSON schemas, and messages.
Claude Code assembles its system prompt from 110+ conditions. One community measurement on v2.1.84 put the cost of existing at roughly 27,169 tokens in an empty directory: about 10,172 for built-in tools, 2,800 for the core prompt, 1,500 for skills, 1,000 for the MCP catalog, 500 for plugins. That is a number from a Reddit teardown, not from Anthropic, and it is version-bound. Treat it as an order of magnitude.
Pi’s bet is the opposite: system prompt plus tool definitions under 1,000 tokens, on the theory that a frontier model does not need the scaffolding. Its README is explicit about what it leaves out. “It intentionally does not include built-in MCP, sub-agents, permission popups, plan mode, to-dos, or background bash.”
Much of the behavior lives in tool descriptions. Claude Code’s Bash description runs to several hundred words. OpenHarness’s is one line, "Run a shell command in the local repository." One HN commenter on a Claude Code teardown put it as “the magic is in the prompts (1200 lines of system-prompt, tool-descriptions, system-reminders), not the tool implementations (400 lines of code) nor the core agent itself.”
Tool calling and execution, end to end
Claude Code’s pipeline has five stages: identification in the SSE stream, collection into a toolUseBlocks[] array, scheduling, execution where hooks and permission checks live, and collection into toolResults[].
Scheduling: up to 10 read-only calls run concurrently; writes serialize. OpenHarness fires everything in one asyncio.gather with no read/write distinction and no concurrency limit, so two concurrent edits to the same file race.
Protocol repair: if a tool call is interrupted, the API rejects your next request, because every tool_use needs a matching tool_result. Claude Code synthesizes error results for orphaned calls. OpenHarness learned the same lesson and left a comment about it:
Use return_exceptions=True so a single failing tool does not abandon its siblings as cancelled coroutines and leave the conversation with un-replied tool_use blocks (Anthropic’s API rejects the next request on the session if any tool_use is missing a matching tool_result).
Edit formats: how a coding agent writes code
How the model expresses a file change is the design decision with the largest measured effect and the first thing to inspect when evaluating a harness.
Four families: whole-file rewrite, a SEARCH/REPLACE block in the text, a unified diff, or a structured tool call with a uniqueness constraint.
Aider is the reference here because it ships five formats and picks one per model. Switching GPT-4 Turbo to unified diff moved it from 20% to 61%. Disabling flexible patching produced 9x more editing errors. Aider documents the formats and which model each targets.
Claude Code’s Edit requires the search string to match exactly once and returns the match count if it does not, forcing the model to expand context. OpenHarness has the same tool shape and no uniqueness check:
if arguments.replace_all:
updated = original.replace(arguments.old_str, arguments.new_str)
else:
updated = original.replace(arguments.old_str, arguments.new_str, 1)
First match, silently. A model targeting return None in a file with forty of them edits line 12 instead of line 900 and gets told Updated /path/to/file. A safe operation became a plausible-looking wrong edit that reports success.
It is also missing the read-before-edit precondition, where Claude Code refuses to edit a file the session has not read, and any mtime check for staleness.
Memory: four mechanisms that often get called one thing
“Memory” covers at least four different things in these tools, with different authors, different loading rules and different lifetimes. Anthropic’s docs state it directly: “Claude Code provides two persistent memory systems: CLAUDE.md files (user-authored instructions) and auto memory (Claude-authored notes).”
1. Instruction files: you write them, they shape behavior. This is the layer the AGENTS.md convention covers. OpenAI started it in August 2025 and donated it to the Agentic AI Foundation on 2025-12-09, the same day MCP went there. The Linux Foundation says more than 60,000 projects have adopted it, a press release number with no methodology behind it.
There is no formal specification. A community spec was proposed as issue #24 in August 2025 and never adopted; the site’s own description is “AGENTS.md is just standard Markdown. Use any headings you like.” So the precedence rules differ between tools that all claim support:
agents.md: “The closest AGENTS.md to the edited file wins.”
Codex: concatenated root to cwd, closer files override earlier guidance, 32 KiB cap.
Claude Code: all discovered files concatenated, no overriding, filesystem root to working directory.
Cursor: nested instructions combined with parent, more specific wins.
Winner-take-all, concatenate-with-override and concatenate-without-override are three different semantics for one convention. Write an AGENTS.md in a subdirectory expecting it to replace the root one and that happens in some tools and not others. Claude Code reads its own CLAUDE.md and interops through an @AGENTS.md import.
How they load matters as much as what they say. Claude Code assembles root and global files eagerly, then loads subdirectory files lazily, triggered only when the Read tool touches something in that directory, wrapped in a system-reminder inside the tool result so the system prompt stays byte-identical and cached. That is the render-order constraint from earlier showing up as a design decision.
2. Agent-authored notes: the model writes them, and they are retrieved, not always loaded. A different mechanism with a different failure mode. Claude Code’s auto memory keeps a machine-local MEMORY.md index, loads its first 200 lines or 25 KB at session start, and pulls topic files on demand; it is shared across worktrees. Codex keeps local memories under ~/.codex/memories/, separate from AGENTS.md and off by default. Cline ships a Memory Bank of structured markdown.
OpenHarness went furthest. Its project memory is markdown with YAML frontmatter carrying importance, ttl_days, supersedes and a content signature for dedup, stored outside the repo under a hashed directory name. Retrieval is keyword scoring with a recency boost, so only the index loads eagerly and bodies get pulled per turn against the current message. It also runs a background consolidation pass in a forked process to merge and prune, and a team vault that runs six secret-scanning regexes before accepting a write.
The distinction that matters for anyone building: instruction files are pushed into every request, agent-authored memory is pulled when it looks relevant. The first costs you context on every turn. The second costs you a retrieval decision that can be wrong.
3. Session state: what lets you resume. JSONL on disk for Claude Code under ~/.claude/projects/, SQLite for Goose and Crush. Pi stores sessions as a tree with entries linked by id and parentId, so forking is branching in place rather than copying. This is also your only audit log.
4. What survives compaction: not a store, a policy, and it is the one people discover by accident. Claude Code re-injects project-root CLAUDE.md, unscoped rules and auto memory from disk after a compaction, and re-reads up to the five most recently modified files. Nested CLAUDE.md files reload when Claude next reads something in their directory. Anything held only in the conversation is gone.
That last point is the practical rule: durable state belongs in a file, because a file gets re-injected and a summary gets lossy.
Context management, context rot and compaction
Context rot is the degradation in quality as the window fills, well before it reaches the limit.
Lost in the Middle (Liu et al., TACL 2024) found a U-shaped curve, with over a 20% drop for information in the middle of the context, falling below closed-book performance in 20-30 document settings. NoLiMa (ICML 2025) tested 13 models with 128K+ advertised context and found that at 32K tokens, 11 of the 13 dropped below half their short-context baseline. GPT-4o went from 99.3% to 69.7%. RULER found only half of models claiming 32K held up at 32K.
Chroma’s Context Rot study named the effect after testing 18 models across 194,480 calls. One result: shuffled, incoherent haystacks consistently outperformed logically structured ones. That creates a problem for compaction, which restructures context.
Compaction is the main mitigation. Anthropic defines it as “taking a conversation nearing the context window limit, summarizing its contents, and reinitiating a new context window with the summary.”
The triggers, from source:
Claude Code:
contextWindow - min(maxOutputTokens, 20,000) - 13,000; on a 200k window that is 167,000, on 1M it is 967,000.OpenCode:
COMPACTION_BUFFER = 20_000, fires attoken_count >= context_limit - 20_000, keeps 8,000.Pi:
reserveTokens16,384,keepRecentTokens20,000.Goose:
GOOSE_AUTO_COMPACT_THRESHOLD, default 0.8.Gemini CLI:
compressionThreshold, default 0.5.Crush: 200,000 with a 20,000 buffer and a 0.2 ratio.
OpenHarness:
contextWindow - 33,000, always.
Two shapes: an absolute buffer below the window (Claude Code, OpenCode, Pi, OpenHarness) or a ratio of it (Goose, Gemini CLI, Crush). The absolute forms reserve room for the model’s output; the ratios reserve a fraction of the window regardless of it.
Claude Code runs five named stages: Snip Compact, Microcompact, Auto Compact, Reactive Compact and Context Collapse. Microcompaction uses no model at all. It replaces old tool-result bodies with [Tool result cleared], keeps the three most recent, and fires when clearable tool-result tokens exceed 20,000. It is free, deterministic and buys a lot of room.
OpenHarness runs three non-LLM stages before calling a model, more layers than any other harness here.
Compaction is lossy: the vendors document it. Anthropic concedes it “doesn’t always pass perfectly clear instructions to the next agent.” One user reported roughly 8K of DOM markup being summarized down to the sentence “user provided DOM markup.” Claude Code tombstones cleared results so the model knows something was there.
Amp took the opposite position and shipped Handoff instead of compaction, on the argument that an explicit boundary beats a silent lossy one.
A study of 150 SWE-Bench Verified trajectories put observation masking against LLM summarization: masking solved 54.8% at $0.61 an instance, summarization 53.8% at$0.64. Dropping old observations matched writing a summary for less money.
The result reverses at the tightest budgets, where summarization scored 72.8% against FIFO’s 42.2%. A gated hybrid follows: mask by default, summarize when the context is out of room.
OpenHarness measures context with max(1, (len(text) + 3) // 4) and a 4/3 padding factor, so roughly characters over three. It uses no tokenizer and ignores the real usage numbers the API returns even though it tracks them elsewhere. The number its entire compaction system depends on is an estimate.
Sandboxed execution, permission modes and guardrails
Permission rules are enforced by the harness, not the model. Claude Code’s docs say so directly: “Permission rules are enforced by Claude Code, not by the model.” Rules resolve to allow, ask or deny, with deny and ask beating allow.
It cannot live in the prompt because prompt guardrails degrade as context grows. Protections have to sit somewhere the model cannot forget them.
Permission modes are a ladder from read-only to bypass, and deny wins at every rung. Claude Code’s six name the rungs: plan (read-only), default (ask on first use), acceptEdits, auto (classifier-gated), dontAsk, bypassPermissions, and deny rules block even in the last one. Aider takes a different approach: no permission system, git as the gate, human-in-the-loop.
Pi refuses the whole category: “Since we cannot solve this trifecta of capabilities (read data, execute code, network access), pi just gives in.” Containment is delegated to a Linux micro-VM, Docker or OpenShell instead.
Sandbox backends across the field: Seatbelt on macOS, Docker or Podman, gVisor, LXC/LXD, Windows Native Sandbox, and bubblewrap on Linux.
The sandbox wraps Bash subprocesses, not the harness process. A Claude Code teardown describes the boundary:
The sandbox wraps Bash subprocesses. It does not wrap the Claude Code process itself. The Write, Read, and Edit tools run in-process: Node.js calling fs.writeFile, no subprocess, no seatbelt, no bubblewrap.
OpenHarness has the same gap. Its file tools do call a path validator, but only inside an is_docker_sandbox_active() branch, and the default backend is Anthropic’s srt. So on a default configuration with sandboxing fully enabled, that check never runs. Even under Docker the write still lands on the host in-process; an identical-path bind mount is what makes it look coherent.
Claude Code’s auto mode is the most documented version of a classifier gate. Its classifier is reasoning-blind by design: it sees only user messages and tool calls, with Claude’s own messages and tool outputs stripped out. Hostile content in a repository therefore cannot argue its way into an approval. Anthropic reports a stage-1 false positive rate of 8.5%, cut to 0.4% by a second chain-of-thought stage, and it escalates to a human after 3 consecutive denials or 20 total. They also report sandboxing reduced permission prompts by 84% in internal use.
On the static side, Claude Code runs 23 named checks in bashSecurity.ts plus AST-first analysis of Bash, and the file carries its own disclaimer: “This is NOT a sandbox... It answers exactly one question: ‘Can we produce a trustworthy argv[] for each simple command in this string?’”
OpenHarness has no Bash analyzer at all. No AST parse, no classifier, no dangerous-command list. The only command-level control is user-supplied fnmatch globs, defaulting to empty.
Sessions, checkpoints and rollback
Recovery is a harness feature.
Claude Code takes a checkpoint per user prompt and keeps the 100 most recent per session. Cline and Gemini CLI use a shadow git repository instead, so rollback is a git operation against a repo the user never sees. Aider skips the abstraction entirely and commits to your actual git history, which is the most transparent option and the most opinionated.
Pi stores sessions as a tree, with entries linked by id and parentId in JSONL under ~/.pi/agent/sessions/. /fork and /clone branch in place rather than copying, so you can walk back to any point and take a different path without losing the first one.
Session storage formats are also the only audit log: JSONL for Claude Code and Pi, SQLite for Goose and Crush.
Extensibility: MCP, skills and hooks
The decision is where the extension boundary sits and what it costs in context. Every tool a server exposes is a description loaded into the window before the agent does anything. Pi’s arithmetic: Playwright MCP is 21 tools and 13,700 tokens, Chrome DevTools MCP is 26 tools and 18,000, “7-9% of your context window gone before you even start working,” against 225 tokens for a README describing the same capability. OpenCode users report roughly 5,000 tokens for a GitHub server, 2,000 for PostgreSQL, and 40,000 to 70,000 for nine servers before a single prompt; lazy loading was requested and not implemented.
Pi’s answer is to keep MCP out of the core and give the agent Bash plus CLI tools with READMEs, on a second argument as well as the token one: “MCP servers also aren’t composable. Agents can run Bash and write code well.” Goose’s answer is the opposite, MCP as the only boundary, with its own built-ins shipped as MCP servers so one abstraction covers everything; Block built 60+ internal servers on it. Claude Code, Cline, OpenCode, Crush and Gemini CLI sit between, with MCP for external tools and native hooks for policy; Claude Code adds deferred loading so descriptions stay out of the window until the model asks for them, and OpenCode has not.
The current spec revision is 2026-07-28, and it made the protocol stateless: the initialize handshake is gone, every request carries its own protocol version and capabilities, and protocol-level sessions were removed entirely. Cross-call state now uses server-minted handles passed as ordinary tool arguments. Two transports: stdio and Streamable HTTP. Claude Code’s list of four MCP transports includes a deprecated sse and a ws that the spec never defined.
The architecture doc makes the case that MCP is a harness concern. The spec assigns managing clients, permissions, security, authorization, sampling and context aggregation to the host. The model does not appear in the architecture. The security requirements also address the host: “Hosts must obtain explicit user consent before invoking any tool,” and “Tools represent arbitrary code execution and must be treated with appropriate caution.”
One implementation detail carries it: MCP tool wrappers implement the same internal Tool abstraction as the built-ins. Scheduling, permissions and hooks treat them identically. Claude Code namespaces them mcp__<server>__<tool>, caps descriptions at 2,048 characters, and carries annotations like readOnlyHint and destructiveHint. OpenHarness, independently, uses the identical mcp__server__tool naming and the same shared abstraction.
MCP security: tool poisoning and line jumping
Tool descriptions are loaded into your context before you invoke anything, which makes them an injection surface.
Invariant Labs named tool poisoning in April 2025: instructions hidden in tool descriptions, visible to the model and not to you. Two variants: rug pulls, where a description changes after you approved it, and cross-server shadowing, where a malicious server poisons the descriptions of a trusted server’s tools.
Trail of Bits documented four more modes that month. Line jumping is the structural one: tools/list responses load descriptions before any tool is invoked, so a malicious description bypasses user approval entirely. Their mitigation is trust-on-first-use, alert when a description changes. They also found conversation-history theft through trigger phrases, payloads obfuscated with ANSI escape codes, and long-term API keys sitting in world-readable files.
The incidents follow the same shape. Invariant showed a GitHub MCP attack where a malicious issue leads the agent to leak private repo data through a public PR, and called it “an architectural issue, not code flaw.” Anthropic’s own Slack MCP server was archived two days after a report that invisible injection could make Claude Code post a link containing .env secrets, where Slack’s link unfurling did the exfiltration. The channel was a UI feature.
Claude Code has two CVEs in this family: CVE-2025-55284, where an overly broad safe-command allowlist let an attacker bypass confirmation and exfiltrate a file, and CVE-2026-24887, a parsing error that allowed the same thing through find, fixed in v2.0.72. Microsoft 365 Copilot’s EchoLeak was zero-click through a crafted email.
Every one of those mitigations is a harness feature. The model cannot tell a poisoned tool description from a real one.
Skills work the same way: a SKILL.md folder with frontmatter, where only the name and description enter the system prompt and the body loads when the agent asks for it. Progressive disclosure follows the same principle as deferred tool loading: let the agent pull instead of pushing everything up front. Claude Code has a Skill tool, Cline added them in v3.48.0, and Crush supports the agentskills.io standard.
Hooks are where policy lives. Claude Code exposes around 34 events with handler types including command, http, mcp_tool, prompt and agent, and exit code 2 blocks. OpenHarness has ten events but four handler types where Claude Code has one, and supports priority ordering, which is a genuine improvement.
OpenHarness’s README advertises PreToolUse and PostToolUse, Claude Code’s spelling. Its enum values are pre_tool_use and post_tool_use, and its loader does except ValueError: continue. Copy a Claude Code hooks config into it and you get something that loads without error, reports no problem, and does nothing.
Subagents and multi-agent coordination
Delegation is a harness concern.
Clean context: the subagent starts a new conversation with nothing from the parent. Kotrotsos, on Claude Code v2.0.76: “It starts with a clean 500-token context instead of inheriting 45,000 tokens of unrelated conversation.”
Fork: the subagent inherits the parent’s pre-rendered system prompt and full request prefix, purely for cache economics. The first fork is cold, the rest are cache hits.
Claude Code caps concurrent subagents at 20 and has a depth limit that has moved between 1 and 5 across releases. Handoff is via the last assistant message only, truncated above 50,000 characters. OpenCode ships General, Explore and Scout at default depth 1, and its task permission can remove a subagent from the tool description entirely.
Read-only tool subsets are common: Claude Code’s Explore agents get Glob, Grep, Read and LS. Crush has a read-only AgentTask.
Anthropic’s research system reports an Opus 4 lead with Sonnet 4 subagents beating single-agent Opus 4 by 90.2%, with parallelization cutting research time up to 90%. Token usage alone explained 80% of the performance variance. Anthropic also names a limit: “some domains requiring all agents share same context or involving many dependencies between agents not good fit for multi-agent systems today.”
An equal-budget study found that when thinking tokens are held constant, single-agent systems consistently match or beat multi-agent ones on multi-hop reasoning. It concluded that reported advantages are “better explained by unaccounted computation and context effects rather than inherent architectural benefits.”
A matched-compute study across 260 configurations found results ranging from +80.8% to −70.0% depending on task and coordination style. Independent agents amplify trace-level errors 17.2x through unchecked propagation; centralized coordination holds it to 4.4x. Tasks where single-agent accuracy already exceeds 45% show negative returns from adding agents.
Cognition’s Don’t Build Multi-Agents describes the operational problem: “decision-making ends up too dispersed and context isn’t shared thoroughly enough between agents.” Cursor reported 20 agents slowing to an effective throughput of 2-3 through lock contention.
Delegation helps when work is separable and compute is abundant. It hurts when the work has dependencies, and the failure is quiet.
OpenHarness is the cautionary version. It has no depth limit and no concurrency cap, each subagent is a full Python process, and its coordinator prompt says “Parallelism is your superpower... look for opportunities to fan out.” A spawned worker keeps the agent tool and can spawn its own. That is a fork bomb with good intentions.
The open-source agent harness landscape in 2026
OpenClaw: what a non-coding harness adds
OpenClaw is the one product in the set built for people rather than code: a personal assistant that meets its user in chat apps. Read at commit 46018ec, it shows which parts a harness grows once it leaves the terminal.
Channels: one long-lived Gateway process owns every messaging surface, with each channel as a plugin behind one contract: an inbound monitor, an outbound adapter with per-channel chunk limits, and declared capabilities. The repository ships 27 channel plugins, 23 of them for platforms people use; the docs catalog lists 32. Routing to an agent is deterministic across eight tiers, and the docs say so plainly: “The model does not choose a channel; routing is deterministic”.
Admission before the model: every inbound event gets a verdict before any model call (dispatch, observe only, handled, or drop), after being written to SQLite and checked against a 20-minute dedupe window. A message that arrives mid-run is folded into the active run by default.
Sessions: by default every direct message, from every channel, lands in one main session. Group rooms stay isolated, and the main session watches them through notices “coalesced per conversation, never one wake-up per message”. A session can hold several people, but the docs call ownership and visibility “usability features, not security boundaries”; isolating tenants means separate Gateways.
Proactive work: a heartbeat wakes the main session every 30 minutes by default, reads instructions the agent can rewrite for itself, and replies NO_REPLY when nothing needs attention. Running heartbeats in isolated sessions cut a run from “\~100K tokens down to \~2-5K”, per the docs. Wakes are spaced at least 30 seconds apart, a flood guard trips at five in a minute, and automations disable themselves after 10 consecutive failures.
Approvals: an exec approval binds to the exact argv, working directory, agent, session, a hash of the environment and the SHA-256 of any script it runs, and the node re-checks all of it just before execution. Pending approvals expire after 30 minutes and fall back to deny, and a request with no reachable approver is denied at once. Approvals reach people through Slack, Telegram and Discord buttons, iOS push and the Apple Watch.
Continuity: before each compaction the agent flushes durable notes to a dated file, and a nightly “dreaming” pass consolidates memory, rejecting any rewrite that deletes more than 25% of prior entries.
Gaps: exec defaults to full permissions with prompts off, so an install without a chosen mode never asks. The message tool, which sends to people, has no approval gate; “never send without approval” lives in instruction files the model is asked to follow. Loop detection is off by default, the run timeout is 48 hours, and cost is reported but never enforced. A code comment records a single heartbeat firing “761-1384 paid Anthropic calls in 60 seconds, costing $20-30 per incident”.
Evaluation: 552 scripted scenario files check outcomes with string and artifact assertions. The only model-judged evaluation scores persona style, and nothing measures whether the assistant chose well on input it was not scripted for.
The state of agent harness architectures
A few shapes sit under the eleven products, and the shape determines what you can build on top far more than the features do.
The monolithic CLI: one process, terminal in, terminal out; Aider and Pi. Everything lives together: loop, tools, UI, session storage. It is the easiest to read and run, and the hardest to embed anywhere that is not a terminal. Pi makes this a virtue by keeping the core small enough that extensions can reach into it directly.
The client/server split: OpenCode is the reference. The server runs on 127.0.0.1:4096 and publishes an OpenAPI 3.1 spec at /doc; the TUI is one client among several. There are SDKs in TypeScript, Go and Python, and third-party clients for Neovim, IntelliJ, VS Code and iOS built against that protocol rather than against the internals. Inside the process, the TUI and server run on different threads with events flowing over a bus and RPC.
Pick this shape if the agent has to appear anywhere other than a terminal. Once the loop is behind a protocol, the interface stops being a rewrite.
The layered library: Cline splits into @cline/shared, @cline/llms, @cline/agents and @cline/core. agents is a stateless execution loop; core is the stateful runtime that owns sessions, checkpoints and telemetry. That boundary is the most reusable idea in any of these codebases: the loop becomes a pure function over messages and tools, and everything that makes it hard to test lives on the other side of the line. It also explains how Cline ships to five delivery targets from one core.
The IDE extension: Cline again, and Cursor outside this scope. The harness lives inside an editor’s extension host, which buys you the editor’s own context (open files, selection, diagnostics) and costs you the terminal.
Subprocess-per-agent: every multi-agent implementation here works this way. OpenHarness spawns python -m openharness --task-worker per subagent, passing about fifty environment variables and some CLI flags, with no shared memory. Claude Code’s subagents use conceptually the same isolation, with better economics through fork mode. A crashed subagent cannot corrupt the parent; the cost is that handoff is a string.
Language choice tracks distribution, not performance. TypeScript dominates (OpenCode, Pi, Cline, Gemini CLI) because npm is how developers install things. Goose is Rust, Crush is Go, Aider and OpenHarness are Python. None of these are compute-bound; they spend their time waiting on a model. The language is a packaging decision.
Delivery surfaces: everyone converged on a TUI for humans, a headless or print mode for CI, and increasingly an SDK or server for embedding. Gemini CLI defines distinct exit codes for headless use. If you are building, assume all three from the start: retrofitting headless onto a TUI-shaped codebase means untangling every place the loop writes to a screen.
The open architectural question is the browser. Every harness here assumes a local filesystem and a local shell, which is why none of them drop unchanged into a hosted product. That is the problem I am working on.
Decisions worth copying from these codebases
Separate the stateless loop from the stateful runtime: Cline splits @cline/agents from @cline/core. The loop becomes a function over messages and tools, and sessions, checkpoints and telemetry live on the other side of the line. It is why Cline ships to five delivery targets from one core.
Make compaction a loop outcome, not a side effect: OpenCode’s processor returns continue, compact or stop. Most others run compaction as an imperative preamble at the top of each turn and hope. The difference shows up when compaction fails: an outcome you can branch on, versus an exception in the middle of a turn.
Ask whether truncation is cheaper before you summarize: OpenClaw computes whether trimming old tool results frees at least 1.5x the overflow, and if so skips the model call entirely. Every other harness treats compaction as the response to context pressure. This is the only one that prices the alternative first.
Queue writes by file, not by tool class: Claude Code batches read-only tools and serializes writes. OpenClaw queues on the resolved path, so two edits to the same file serialize and two edits to different files run concurrently. Same safety, strictly more parallelism.
Put the uniqueness constraint in the edit tool: Claude Code’s Edit refuses an ambiguous match and returns the count, forcing the model to expand context. OpenHarness kept the tool shape and dropped the check, so it replaces the first match and reports success. That is the difference between an error and a plausible-looking wrong edit, and it is four lines of code.
Give rules three cost tiers, not one: instruction files are all-or-nothing: full body in context every turn. oh-my-pi splits rules into always-apply, an on-demand index, and conditions that cost nothing until a regex or AST pattern matches the model’s own output mid-stream. Most conventions do not need to be resident.
Move curation off the reply path: OpenClaw’s argument is that “what degrades memory systems is unreliable write-time curation,” so consolidation runs as a scheduled background pass with promotion gates and an anti-amnesia guard that rejects a rewrite deleting too much. OpenHarness does the same thing with a forked child process. Neither asks the model to maintain memory while it is working.
Let the agent propose, make something else decide: OpenClaw’s Skill Workshop turns a new skill into a queued proposal a human applies. Its custodian skills gate self-administration to one privileged agent behind a Gather-Mutate-Repair-Prove-Report contract.
Use the language server, not grep, when one is available: oh-my-pi makes it a prompt-level directive, “NEVER search/manual-edit for code intelligence”, gated on a server resolving, and wires LSP into the write path so a rename updates re-exports before the file moves.
Delete process spawns from the hot loop: oh-my-pi’s \~80k lines of Rust exist so that grep, sed, find, jq and git are in-process function calls rather than forks. For an agent doing hundreds of these per session, that is the dominant latency term.
And the one everybody learns the hard way: every tool_use needs a matching tool_result or the API rejects your next request. Claude Code synthesizes error results for orphaned calls; OpenHarness left a comment explaining the same lesson. If you build a loop, build the repair path with it.
Further reading
Primary definitions:
Birgitta Böckeler, Harness engineering for coding agent users. Guides and sensors, harnessability, harness templates.
Vivek Trivedy, The Anatomy of an Agent Harness. The component enumeration and the co-evolution argument.
Yao et al., ReAct: Synergizing Reasoning and Acting in Language Models.
From the labs:
Anthropic, Effective context engineering for AI agents and Effective harnesses for long-running agents.
Vercel, We removed 80% of our agent’s tools.
Aider, edit formats and repo map.
Thorsten Ball, How to build an agent. A concise explanation of the loop.









