← All posts

The coding-agent harness: how OpenCode, Cursor, and the others are built

A coding agent is a program wrapped around a language model: the loop, context, tools, permissions, session state, and subagents. A walk through OpenCode's docs and dev branch, plus the public contracts of Cursor, Claude Code, and Codex.

Abstract illustration of a coding-agent harness: dark navy background, teal glow, layered architecture plates and a loop around tools
Contents

The form saves, and the server answers with HTTP 500. One sentence goes into the chat. What happens next is not "the model wrote some PHP." The program opens the project, finds the controller, reads the related code, edits a file, runs a check, and, if the check is red, feeds the failure into the next request.

In this shop the language model fills out the work order. A different piece of code touches the bench: files, the terminal, git, the language server. Until that order is carried out and the result is back in the conversation, the model knows nothing about the repository beyond what the program already put in the request.

This piece is about that harness. The open drawing comes from the OpenCode repository (dev, early November 2026) and the docs. Cursor, Claude Code, and Codex are visible only through their public contracts. Their internal trees are not published. How an IDE assembles context at all is already covered in how AI IDEs work with code. The question here is narrower: which programs sit around the model that walks itself to a patch.

Key takeaways

The model does not open the file. It returns a structured request. Disk, shell, and patch belong to the application. Anthropic states the contract directly: the app describes tools, the model chooses a call, application code runs it and returns the result.

One reply almost never fixes the form. The agent loops: gather context, ask the model, run the tool, return the result, repeat. It stops when the model ends the turn, the step limit hits, or a person interrupts the session.

The program builds context, not the weights. Text search, file reads, an index, vector search, and a language server answer different questions. To change a function you want callers and tests, not only the function body.

Three locks do not substitute for each other. The model instruction, the application's permission table, and process isolation at the operating-system level fail for different reasons. A sentence in the prompt does not stop a command if the application still launches it.

Several agents share the task and collide on files. A researcher, a coder, and a reviewer speed the work up only when they are not writing the same place. Otherwise the time goes into coordination, not the patch.

A small agent is built in layers. A model client and two read tools first. Then the loop. Then writes and a terminal. A sandbox and subagents make sense after one agent can stop on a green check, not on the sentence "done."

The model writes the order, the bench carries it out

Autocomplete finishes the current line. Chat explains the selection. A coding agent takes a task and walks the repository until there is a change and a check. The difference is not a "smarter model." The difference is a program around the model, with state, tools, and the right to say no.

A customer says the form fails with 500 after save. The dispatcher does not grab the torch. They write an order: look at the controller, read the data conversion, run the test. The model sits where the dispatcher sits. The bench, the tool crib, and the shift log belong to the application.

The durable parts are the same in a terminal agent, an editor agent, and an agent in a remote environment. The shell changes. The duties do not.

Part In the shop In the agent
Interface The counter Terminal, editor, desktop app, web
Dispatcher Runs the shift and the order Task loop, state, cancel
Model Decides which order to write A call to a provider API
Context Drawings on the table Files, search, history, project rules
Tools The crib Read, edit, search, terminal, external services
Permissions Who may use the torch Allow, ask, deny
Bench Part, machine, store Files, terminal, git, diagnostics
Log The shift and the rollback Session, history, snapshots, compaction

These are roles, not a directory tree. In OpenCode some of them live in packages/core. The interface is split across packages/tui, packages/app, and packages/desktop. Model providers are loaded from packages/ai. A closed product can keep the same roles inside one process.

The model is one node. Behavior that looks like "the agent understood the project" is usually ordinary code: what to put in the request, which tool is allowed, when to stop, what to keep between steps. The split between an explicit program and a trained model is covered in program versus trained model.

The loop keeps turning until the form stops failing

The simplified loop below is a teaching sketch, not OpenCode.

async function runAgent(task: string) {
  const messages = await prepareContext(task);

  while (true) {
    const response = await llm.generate({
      messages,
      tools: toolRegistry.definitions(),
    });

    if (response.stopReason === "end_turn") {
      return response.content;
    }

    if (response.stopReason === "tool_use") {
      for (const call of response.toolCalls) {
        const result = await executeTool(call.name, call.arguments);
        messages.push(response, result);
      }
    }
  }
}

On "HTTP 500 after the form saves," a good run almost never starts with a patch. Search for the controller and the route. Read the input conversion. Form a hypothesis: the cast is failing, not the database write. Then edit. Then test. If the test is red, the error text re-enters the loop and the hypothesis changes. An eight-step slide pretends the path is a straight line. The agent goes back to search, drops the first guess, and sometimes writes a test the original sentence never mentioned.

A real loop is thicker. Streaming, several calls in one turn, cancel, retry when the provider fails, a step cap, and history compaction. In Anthropic's docs the client-tool loop is keyed on stop_reason: while it is tool_use, the application runs the calls and sends tool_result. The loop also exits on end_turn, on a token limit, and on a refusal.

On the dev branch of anomalyco/opencode this lives in packages/core/src/session/runner/llm.ts. The drain loop prepares a step boundary, runs the step, and increments a counter. Before the request it estimates context size. If the context does not fit, compaction runs. If the agent has a step limit and the counter has reached it, the request gains a limit notice and tool choice is turned off (toolChoice: "none"). That turn must wrap up. It cannot call tools. A separate path retries when the provider errors.

A failed command is more useful than a smooth "done." A red test returns a fact. The model can read it and change its mind. It can also spin: the same search, the same edit, the same red check. OpenCode's permission set has a guard named doom_loop. Three identical calls with the same input ask the person by default.

Providers belong behind one interface. On dev, OpenCode marks provider packages with the prefix aisdk: and loads them as modules. Context windows, tool-call formats, and prices still differ. A cheap model fits classification and short housekeeping turns. A strong one fits the design of the edit. A local one fits a fragment that must not leave the machine. The bill for those laps is the cost of a finished task, not of one request. That accounting is in the token economics of coding with a model.

The repository does not enter the request by itself

The weights do not contain your repository. If the program did not put a file in the request, the file is absent for that turn. "The agent studied the project" always means some code chose what to show.

Text search is enough to find a function. OpenCode runs grep and glob through ripgrep and respects .gitignore by default. That is cheap and exact when the name, the line, or the path is known.

"Who else breaks if this cast changes?" is a different question. You want callers, the interface, dependencies, and tests. A language server answers part of it: definition, references, call hierarchy. OpenCode's docs mark the lsp tool as experimental and gated by a flag. It can go to definition, find references, list document symbols, and report incoming and outgoing calls. That is not a string search.

Vector search helps when a person described behavior in words and the function name never surfaced. It is a poor replacement for an exact identifier, and it will happily drag in a neighboring module that is "about the same thing." A full-repo index pays off on a large tree and on repeated questions. On a small service the agent usually reaches the file with two calls, glob and grep, and no vector database. How document search behaves in a real system is covered in production RAG.

Project rules are a separate input. OpenCode's /init command writes AGENTS.md: how to build, how to check, which conventions are invisible in file names. ~/.config/opencode/AGENTS.md adds personal rules. If there is no local file, CLAUDE.md is read. That is not memory of past sessions. It is an instruction the program mixes into every new shift.

What the agent is asking What should answer
Where is the controller? Path and text search
What is inside the function? A file read, preferably a line range
Who calls it? A language server, or a careful symbol search
Where is similar behavior without an exact name? Vector search, then a read to confirm
How does this repo expect to be checked? AGENTS.md and the other instruction files

The application calls the tool, not the model

The model cannot open the disk. It can return a tool name and arguments that match a schema the application put in the request. Six steps follow, and all of them are code: describe the schema, hand it to the model, accept the choice, check permission, execute, return the result.

The public OpenCode docs name the built-ins read, write, edit, apply_patch, grep, glob, bash, lsp, todowrite, webfetch, websearch, question, and skill. File mutation sits under one permission, edit. On dev in early November 2026 the modules are named a little differently: shell.ts instead of bash, patch.ts instead of apply_patch, subagent.ts where older pages say task. Check the names against the version you run. The roles match.

edit replaces an exact string. write creates a file or overwrites it. patch carries several operations in one text: add, update, move, delete. question stops and asks a person. skill loads the text of a skill. It does not, by itself, run arbitrary code.

There is an exception to "the model runs nothing." Some Anthropic tools run on the provider: web search, page fetch, code execution in their environment. The application does not build tool_result for those. The exception does not cover files in your repository. Your process still touches those.

Custom tools in OpenCode are a schema plus an execute function. A plugin tool with the same name as a built-in replaces the built-in. External services arrive separately, through MCP, and show up beside the built-in calls. The protocol is shared. Execution still goes through the same loop and the same permissions.

Three locks: instruction, permission, isolation

Picture a command that wipes the disk root, or a read of a secret file followed by a send to the outside. "Be careful" in the system instruction does not stop that. The instruction changes what the model tends to request. If the request arrives anyway, the application decides. If the application allows the launch, the process can still be boxed so the damage cannot leave a directory and a network boundary.

OpenCode's Explore agent, on dev, shows both of the first two locks. The system text forbids creating or modifying files and says shell commands must be read-only. Next to it, the permission list denies everything, then allows read, grep, glob, shell, webfetch, and websearch. Reading *.env asks the person. *.env.example is allowed. The sentence and the table agree in intent. Agreement does not make them one mechanism. The model can ignore the sentence. The table rejects the call before anything runs.

Documented permissions resolve to allow, ask, or deny. The last matching rule wins, so the catch-all * comes first and the exceptions after it. That is how you allow git status, ask on git push, deny rm *, and permit edits only under a docs directory. Paths outside the project root hit external_directory, which asks by default. Three identical calls in a row hit doom_loop.

ask offers three answers: once, always for this pattern until the session ends, or reject. --auto approves whatever would otherwise have asked. An explicit deny still holds.

The third lock sits lower. A directory on disk, a separate user, a container, a network with no outbound path, a command log. OpenCode is not that sandbox out of the box. It runs commands in the project environment and relies on permissions. Process isolation is something you add, with Docker or another operating-system boundary. The file snapshot in packages/core/src/snapshot.ts (capture, diff, restore) rolls back the agent's edits. It does not undo a command that already left for the network.

Cursor's public docs describe their own checkpoints: a snapshot of modified files before a large edit, stored locally and apart from git. Restoring a checkpoint restores files and does not erase chat messages. That is a fourth, product-level undo. It still does not replace permissions or isolation.

Adversarial checks of the application around a model are in the LLM red-team playbook. For a coding agent the practical minimum is shorter. A secret is not read without a question. A destructive command is denied by pattern. The project directory is not the whole disk. File rollback has been tried before the agent is given a network.

The session remembers longer than the window

A multi-hour shift does not have to fit in one request. The program stores the person's messages, the model's replies, tool calls, results, the task list, file diffs, and short summaries. The model's window stores only what fit in the latest request.

OpenCode can compact a session. On dev, packages/core/src/session/compaction.ts keeps about 15% of the window free, and at least 16,000 tokens, unless you set another buffer. A tail of the recent conversation is kept verbatim beside the summary. The summary can be computed ahead, in the background, so a full window does not wait for it from scratch. The trigger is automatic, manual (/compact), or an overflow: the provider rejected the context as too long. A hidden Compaction agent exists for this, separate from Build. A plugin can append its own facts to the compaction prompt through the experimental.session.compacting hook.

Compaction drops detail. That is why the tail keeps the latest decisions, and the summary should still say which files were touched and what is unfinished. If the agent searches for the same file again after compaction, the summary was too thin. Fix the summary. A longer window will not repair a bad one.

Hidden agents sit beside this. You cannot pick them in the UI. Title names the session in a short phrase. Summary writes two or three sentences in the voice of a pull-request description. In the source both are stripped of tools. They do not fix code. They pack the log so a person can find the shift later.

The file snapshot and the message log diverge. Restoring a snapshot returns the file tree. The conversation can remain. OpenCode's /undo command, in the docs, reverts the edits and shows the original message again, so you can rephrase and rerun. Know which undo you are pressing.

A second agent shares the work and the files

One dispatcher is enough while there is one order. When a tree search, an edit, and a test run crowd the same window, the work goes to a second agent. The gain shows up if the second agent has its own bench. If both write the same file, the time goes into the seam.

In OpenCode the primary agents are visible and switch with Tab. A subagent is invoked by the primary, or by a person via @. The child session opens separately. You can enter it and return to the parent.

On dev the roles look like this.

Build is the default and runs tools within permissions. Plan is also primary, but edits are denied everywhere except ~/.opencode/plan. A reminder is mixed into context: do not implement until the person switches agents. If an edit hits the deny, the error says the file is outside that directory. The public docs describe a close rule for plan files under .opencode/plans/. Check the path against your version. The intent matches. The directory name in code and in docs may not.

Explore searches the tree and is not supposed to change it. It is also forbidden to call subagents. The description tells the model the depth: a quick pass, a medium one, or a thorough walk. General takes multi-step work and may edit files, but it does not ask the person questions and does not spawn further subagents. Otherwise the chain grows without a watcher.

Parallel runs make sense on independent pieces: one searches for callers, another reads tests, a third reads configuration. A shared file, a shared migration, the same authorization module do not split that way. Git worktrees isolate edits. In packages/core/src/worktree.ts that is its own service, not "just another chat." Two patches on one line are still a conflict.

A coordinator that only hands out tasks costs money too. Each subagent carries its own context. The summary sent upward has to be shorter than the original search, or the parent window fills with someone else's log. Cursor's docs describe a similar move from the outside for large work: a project agent plans and delegates. The page does not show how the handoff is built inside.

Extensions do not replace the loop

Built-in tools are enough while the task stays inside the repository. The database, the tracker, a library's documentation, and monitoring live outside. Connect them so the loop does not have to be rewritten.

MCP adds foreign tools over a shared protocol. In OpenCode a server is either local (a command, its own directory, environment variables) or remote (a URL, headers, OAuth). Tool names are prefixed with the server name, and the same permissions apply, including a pattern such as mymcp_*. Every server inflates the tool descriptions in the request. The docs warn that the GitHub server can consume the window by itself. Enable the server this task needs. Leave the others off.

A skill is different. It is a SKILL.md file with a name and a short description. The model sees the list of descriptions and loads the full text with the skill tool when it needs it. The context does not receive every instruction up front. Skills are discovered under .opencode/skills/, under the compatible .claude/skills/ and .agents/skills/ trees, and globally in the home directory. The skill permission can hide internal skills with a pattern such as internal-*.

A plugin changes the application itself. The tool.execute.before hook sees arguments before launch. Use it to block a .env read or to escape a command. tool.execute.after sees the result. Session events, permission events, and language-server diagnostics are available too. A custom tool registers beside the built-ins. That is not MCP. A plugin lives inside the agent process. MCP brings a foreign server.

A language server fills in structure. It does not replace grep, and it does not grant write access. It answers where a symbol is declared and who calls it, if a server for that language is running.

The OpenCode drawing on the dev branch

The name OpenCode covers two incompatible histories. The repository opencode-ai/opencode was archived on 18 September 2025. Its README says the project continued as Crush, with the Charm team. The current agent with this name lives in anomalyco/opencode, under the MIT license, with a terminal, a desktop app, and an editor extension. Reading the old archive to understand the current loop will not work.

The dev tree in early November 2026 lays out like this. It is a reading map, not a promise that a path will survive the next month.

What you want Where to look
The step loop packages/core/src/session/runner/llm.ts
The step cap packages/core/src/session/runner/max-steps.ts
Compaction packages/core/src/session/compaction.ts
Built-in agents packages/core/src/plugin/agent.ts, plan.ts
Tools packages/core/src/tool/plugin/
Permissions packages/core/src/permission.ts
MCP packages/core/src/mcp/
Skills packages/core/src/skill/
Providers packages/core/src/provider.ts
File snapshots packages/core/src/snapshot.ts
Git worktrees packages/core/src/worktree.ts
Plugin contract packages/plugin

A short reading order helps. Start with agent.ts and plan.ts, so the difference between Build, Plan, and Explore is a permission list, not a slogan. Then one tool, read.ts or shell.ts, from schema to execution. Then the loop in llm.ts: where context is estimated, where compaction is called, where tools are cut off by the step limit. Plugins and MCP then read as an addition to a crib you already understand.

The docs at opencode.ai describe what a person sees: Tab between agents, /init, /undo, the opencode.json config. The dev sources describe how that is assembled. When names diverge (bash and shell, the plan directory), trust the version you run, and keep both names in mind.

Cursor, Claude Code, and Codex are visible from the outside

The internal trees of these three products are not published the way packages/core is. Compare the contract in the docs. Do not invent directories for them.

Cursor's agent overview names three parts: instructions, tools, and the model. Instructions include the system text and rules. Tools search the tree, read and edit files, run a terminal, drive a browser, and ask questions. The docs set no ceiling on the number of calls in a task. Checkpoints snapshot modified files and roll back apart from git. For a large body of work the docs describe a coordinator that plans and delegates. A message can wait in a queue until the current turn finishes, or be sent immediately so it lands on the next tool boundary. That is the same "model, tool, model again" loop, packed into an editor. The page does not reveal how the repository index works. Its place in the IDE pipeline is in how AI IDEs work with code.

Claude Code, from the outside, is the same Anthropic contract. The model returns tool_use. A process on the developer's machine runs the read, the edit, and the command. The result returns to the conversation. Schemas such as the shell and the text editor are trained in, so custom names for the same actions are often called less reliably than the familiar ones. Permissions, the context limit, and the history summary remain the job of the application around the model. OpenCode will even read CLAUDE.md and .claude/skills/ when its own files are absent. Compatible files are not shared source.

Codex, in OpenAI's public description, takes a task against a repository and can run it in an isolated environment, away from your laptop. What you can see is the assignment, the execution boundary, and the returned changes. What you cannot see is the internal queue, snapshot, and model choice. The architectural point is the move itself: the bench can leave the local process, while the log and the review stay with a person.

The shared picture is this. OpenCode shows the seams. Cursor shows the same seams inside an editor, with checkpoints and a coordinator. Claude Code shows a strong tool-call contract on a local machine. Codex shows an execution environment that has been moved out. Picking a product for the job, rather than for the logo, is in Cursor and the alternatives.

A small agent is assembled in layers

You do not need to reproduce OpenCode. A teaching agent in TypeScript is useful because each layer can be switched on alone, and you can see where the behavior breaks. The stack can stay quiet: Node.js, an API client or the AI SDK, Zod for tool schemas, SQLite for the session log. A language server and tree-sitter belong on the structure layer. A container belongs on the isolation layer, not on day one.

  1. A model client. One request, one reply, streaming output. No tools. If this layer lies, the loop only multiplies the lie.
  2. Three read tools: list files, read a range, search text. Short schemas. Paths only inside the project root.
  3. The loop. The model chooses the tool. The result returns to history. There is a step limit and a stop reason.
  4. Writes. Exact fragment replacement first, then file creation. Every edit leaves a diff a person can read.
  5. A terminal. One check command you already consider safe: a test or a build. Output is truncated. The exit code enters the next request.
  6. Context. Project rules in their own file, session history in SQLite, compaction when the token estimate crosses a threshold.
  7. Permissions. Deny paths outside the root, ask on destructive commands, log the calls. Put a container around the terminal when the command is no longer a single test.
  8. A second agent. One searches, one edits. The researcher cannot write. They do not touch the same file in parallel.

The tree can look like this once the layers exist. On day one only llm and cli are alive.

mini-coding-agent/
├── src/
│   ├── agent/
│   │   ├── loop.ts
│   │   └── subagent.ts
│   ├── llm/
│   │   └── provider.ts
│   ├── context/
│   │   ├── builder.ts
│   │   └── compaction.ts
│   ├── tools/
│   │   ├── registry.ts
│   │   ├── filesystem.ts
│   │   └── terminal.ts
│   ├── security/
│   │   └── permissions.ts
│   ├── sessions/
│   │   └── storage.ts
│   └── cli/
│       └── index.ts
└── package.json

Set the finish line outside the model. The build passes. The assigned test is green. The diff does not touch files outside the task. The model's "I fixed it," without that check, does not close the order.

Cost is held by the same handles as in a large agent. A step cap. A short tail instead of the whole history. A cheap model on search, a strong one on the edit. A cache of the unchanged prefix, if the provider offers it. A subagent only when its summary is shorter than the parent searching alone.

A fully local shift uses the same layers: a local model, local search, a local log, a terminal with no network. The compromise is model quality, and the fact that you have to bring up the index and the language server yourself. A map of topics for working without an external API is in the offline engineer roadmap.

Frequently asked questions

How is a coding agent different from autocomplete?

Autocomplete suggests the rest of a line in the open file. An agent searches for files, calls tools, and repeats until the task closes or a limit hits. The model can be the same. The harness is not.

Can the model run a command itself?

Not on your disk. It returns a structured call, and the agent process decides whether to launch it. Some providers run web search and code execution on their own servers. That does not cover files in your project.

Do you have to index the whole repository first?

Not always. An exact name and path are a text search. An index and vectors pay off when the questions are many, the tree is large, and the wording has no identifier. A language server is for relationships between symbols. It does not replace grep.

How do you know the task is finished?

Set the check before the loop: a build command, a test, a limit on the file list. The model's "done" only records that the model decided to stop. A red test sends the loop back to the hypothesis.

Why does the agent spin?

The same call with the same input, a wrong hypothesis, a decision lost in compaction, no stop condition. The remedies are a step cap, a block on a repeated identical call, and an outside check that returns a new fact rather than the same error.

Can a coding agent stay local?

Yes, if the model, the tools, the log, and the execution boundary are local. Edit quality then depends on the local model. Secrets do not leave with the context to a provider.

Are OpenCode and Crush the same project?

No. The opencode-ai/opencode archive has pointed at Crush since 18 September 2025. Current OpenCode is developed in anomalyco/opencode. A shared name does not make the current sources shared.

Do parallel agents speed development up?

Only on independent slices. Two agents in one file spend the turn on conflicts. A subagent allowed to spawn more subagents inflates context faster than the patch grows.

Further reading

Related pieces on this site, from how an environment handles code, to price, to a local setup:

Primary sources for the factual parts: OpenCode source, OpenCode docs, agents and permissions, how Anthropic tool use works, Cursor agent overview, Model Context Protocol.

Conclusion

A coding agent looks like a dispatch desk, not like an oracle in the editor gutter. The model writes the order. Files, commands, and rollback are a program. The quality of the shift depends on what entered the request, which tools were allowed, and which check is allowed to say "not yet."

The next practical step is short. Take a small repository, start OpenCode in Plan, and ask it only to find the cause of one known failure, with no edits. Look at which calls landed in the log. Then switch to Build and compare what changed in the permissions, not in the eloquence of the reply. After that, a teaching loop that reads a file and runs one test has something real to be checked against.

Comments

Loading comments…