The Third Mind: Local AI for When the Cloud Runs Out
OpenCode on open-weight models, running on a MacBook Pro beside Claude Code and Codex: the agent that works when both are out of quota. Memory, prompt caching, uncensored models, self-updating lineups and a coding duel with Claude.
| ⚡ | TL;DR What it is. A third coding agent beside Claude Code and Codex: OpenCode on open-weight models running on a MacBook Pro M4 Max with 128 GB. No paid API, no account, no usage window, and model requests never leave the machine. When it works. Only when both frontier agents are blocked, or when I open it myself, for example for uncensored work that should stay local. Everything else still goes to Claude and Codex, because they are stronger. What made it hard. Not installing it. The hard parts were memory, prompt caching on hybrid models, measuring a laptop honestly, defining "uncensored" and "always latest" so they can be tested, and plugging a third agent into the same rules as the other two. The numbers. In the duel both passed all 48 hidden tests; the local agent committed its work after 43 minutes, Claude after 26. With the same local model, OpenCode passed 77 of 77 hidden tests, Qwen Code 76 and Pi 69. The challenger I measured, Qwen3.8-Flash-Next, wrote about 2.5 times as fast as the 27B I kept, but it exceeded the two-model memory budget beside the fast model, and both models ran out of budget on the same coding task. |
The three-minute manual
This is the operating guide for my harness, in one place. The box at the start of "Lock it to the machine" sets up OpenCode with one local model; the launchers, the request guard and the automation below are parts of my harness on top of that. The model guide after this section goes deeper.
Two jobs. The local agent exists for two situations: uncensored work that stays on this Mac, and work that has to go on when Claude and Codex are both out of quota. Everything else still starts in Claude or Codex, because they are stronger.
oc and ocu are shell functions from my harness, not part of OpenCode. All four start with automatic approval; in OpenCode only explicit denials block an action.
Why these two models. The coder, Qwen3.8-27B, carried the local agent through every hidden test of the duel and the agent bake-off, and it fits in memory beside the fast model. The uncensored model is the same 27B decensored with Heretic: it passed its role's separate coding and tool acceptance checks, and its publisher reports far fewer refusals.
Job 1: uncensored work that stays on this Mac
Run ocu in the project folder, or ocu run "TASK" without a window. It selects the uncensored model and keeps the session local-only: a second opinion comes from a fresh local reviewer, and nothing goes to Claude or Codex unless I ask. In a running oc session, switch to the uncensored agent and say "keep this local" to get both.
Model requests never leave the Mac. The shell, web and MCP tools keep normal network access, so local-only is not a network sandbox.
Job 2: Claude and Codex are both out
- At 95 % of a usage window, the active agent finishes its step, commits, pushes, writes a takeover record and names the next agent: the other frontier agent first, the local one only when both are blocked.
- Open a new tab in the same project, start the named agent (
c,codexoroc) and type takeover from claude or takeover from codex, naming the agent that stopped. It reads the record and continues with the same tests, reviews and verifiers. - When Claude is back, tell the local agent "wrap up for Claude", open
cand type takeover from local. Hand hard steps back early.
ai-workspace takeover status shows who is limited; if nobody is available, wait for the reset the message names. A Ralph loop ("ralph this until it is verified") switches on its own, from Claude to Codex to the local agent; ai-ralph status shows it and ai-ralph stop GOAL stops it. The harness's other loops switch only between Claude and Codex.
Inside OpenCode
Shift+Tab moves to the next agent, and each one brings its own model: build (coding, the default), plan (read-only planning), deep (hard reasoning), fast, uncensored and vision. /models changes only the model, and Ctrl+T switches thinking on or off. Without a window, oc run "TASK" works on the coder and exits; --agent fast or --model local/think picks another.
Updates and undo
Every night at 04:30 an updater looks for newer models for each name, tests a candidate, on a private copy of the server, against the current model, and switches only if the lineup still fits in memory, every gate passes and the candidate scores at least as well. Every run writes a receipt and sends a notification. Homebrew upgrades llama.cpp and OpenCode every day at 10:00.
Limits
No usage window and no bill per token; the limits are memory, a laptop's varying speed and hard problems. The current coder still runs out of its answer budget on one acceptance task, so every gate stays and hard problems go back to Claude or Codex.
The local models: which one, when
Six names, three models. OpenCode only ever asks for a name such as local/coder; the model manager decides which file answers it. Two models fit in memory at once, so moving to a third unloads one, and the first answer after a switch can take a minute.
The 27B: coder, think, vision, small. Qwen3.8-27B is dense: all 27 billion parameters work on every token. The 6-bit build (UD-Q6_K_XL) is 25.3 GB instead of 54.7 GB at 16 bits. One server process serves all four names, so moving between them needs no reload. It writes about 10 to 28 tokens per second and reads about 110 to 210, depending on the prompt and the laptop's state. With OpenCode it passed all 77 hidden tests of the agent bake-off and all 48 of the duel. In its acceptance run it made every tool call exactly and found the planted fact; on coding it passed 5 of 6, and the sixth ran out of its 20,480-token answer budget. Why this one: Flash-Next writes about 2.5 times as fast and scored higher, but at about 75 GiB it does not fit beside the fast model in the 94 GiB the manager allows for two models.
The 35B-A3B: fast. Qwen3.6-35B-A3B is a mixture of experts: for each token a router picks 8 of its 256 experts, plus one shared, so only about 3 of its 35 billion parameters do the work. It still needs memory for all of them (31.2 GiB on disk) but computes far less: in llama-bench it read 1,263 tokens per second and wrote 75, against 206–214 and about 16 for the 27B. Through the production router it measured much lower, 296 and 22.6, a gap not yet explained. It runs without thinking, passed every suite of its acceptance test, and the harness gives it quick edits and questions.
The decensored 27B: uncensored. The same 27B after Heretic removed its refusal direction from the weights; "What 'uncensored' means here" below explains how. It was chosen by rule: a trusted publisher, a published KL divergence of at most 0.10 and at most 5 refusals in 100. This build reports 0.0244 and 3 (91 for the original). On a private copy of the server it then passed every gate of its role: six tool calls, six coding tasks, six ordinary requests that over-cautious models refuse and a fact in a 32,000-token document, and it finished every answer. Its configured context is 65,536 tokens, half the coder's. Use it for requests the others refuse, such as a villain's monologue or security work on your own lab, and for private work with ocu. The drop in refusals is the publisher's measurement: the stock 27B also answered my six requests once it had room to think. Harness rules, permissions and gates do not change.
vision and small. vision adds the 27B's vision projector, an image encoder, so it accepts screenshots, diagrams and photos as input; it cannot generate images. It read both test images of its smoke test but has no full acceptance evaluation yet. Without a window, attach the file: oc run --agent vision -f shot.png "Read the text in the attached image. Reply with that text only." returned a test screenshot's text exactly. small is the 27B with thinking and tools off and a 1,024-token answer limit; OpenCode uses it to name sessions.
Trying other models
OpenCode can reach only these six names; the request guard blocks anything else. Trying a model therefore means putting it behind a name, measuring it, and keeping or undoing the change:
ai-local update --dry-runshows what the updater would propose for each name, without downloading anything.ai-local pull REPO --quant QUANTdownloads a GGUF build from Hugging Face, pinned and hash-checked, registers it and prints its id;--mmprojadds the vision projector,--mtpthe speculative-decoding head.ai-local fit --role coder --model IDchecks that the lineup would still fit in memory.ai-local promote coder IDruns the staged evaluation and moves the name only if every gate of the role passes;ai-local rollback codermoves it back.ai-local eval ROLEandai-local bench ROLEtest the model behind a name: its acceptance suites, or its speed per prompt size.ai-local decline IDstops the updater from proposing a build again;ai-local gc --dry-runpreviews which unused models it would delete, keeping one rollback target per name.
To follow a new model family automatically, add a rule to the role's track list in the model manager's configuration, such as {"author": "poolside", "match": "^Laguna-"}: a Hugging Face publisher and a pattern for its repository names. The next nightly run follows it, and ai-local update --dry-run --role coder shows what it would propose. Today coder follows Qwen 3.6 and newer, Poolside's Laguna and NVIDIA's Nemotron; think and vision follow Qwen; fast and small follow Qwen's mixture-of-experts line; uncensored follows decensored Qwen builds.
How the lineup updates itself
Every night at 04:30, for each name, the updater:
- looks for newer models in the tracked families on Hugging Face and skips any base released less than three days ago;
- picks a build from a trusted quantizer such as unsloth or bartowski, or from an approved publisher for
uncensored, at least 4 bits per weight and within the name's size cap: 100 GB, or 40 GB forfastandsmall; - downloads it pinned to one commit and checks every file's size and SHA-256;
- checks memory: the two largest models must still fit in 94 GiB, otherwise the candidate waits (
does_not_fit); - tests the current model and the candidate in the same run, the candidate on a private copy of the server with production's settings;
- moves the name only if every gate of the role passes, the candidate scores at least as well and the model it replaces is idle, then checks that the server answers under the name, or restores the old lineup;
- writes a receipt, sends a notification, keeps the previous model for rollback and remembers a rejected build so it is not proposed again.
The first unattended run, on 19 October, changed nothing: Flash-Next still did not fit, and no other name had a candidate. A llama.cpp upgrade restarts the server, and a new OpenCode version must pass the guard's attack test before it may start. Nothing re-tests the running models after such an upgrade; ai-local eval ROLE is the manual check. The design and its lessons are in "'Always latest', defined" below.
Why a third mind
My two frontier agents are excellent and rationed. Claude Code runs Opus 5.5 at maximum effort; Codex runs GPT-6 Astra at ultra. Both sit on subscriptions with usage windows that reset on their own schedules; Claude's are five hours and a week. On the afternoon this was built, Claude's five-hour window hit 95 %, then 99 %. The harness did what it is supposed to do: it told Claude to finish the step, push, write a takeover record and name the next agent.
A month ago I wrote about the cheap route: point OpenCode at DeepSeek or GLM and pay for tokens. That article's lesson still holds. The agent, the model, the bill and the gateway are four different things. This time I changed the bill to zero and the gateway to nothing: the models run on the laptop.
The goal was a third mind that cannot run out. It should be local, free, offline for its model calls and able to switch models by purpose. It should update itself the way cloud models do, refuse nothing for my own work, run interactive and headless, and take over when the other two are out. It also had to obey the same rules as the other two, which turned out to be most of the work.
Free means no per-token bill and no quota. The laptop, the disk and the electricity still cost something.
The trio and its rules
Three agents, one set of rules. The question is who works when.
This ordering is deliberate. "Use local when Claude is slow" would quietly lower the quality bar. Local models are good and getting better, but they are weaker on hard problems, so they earn their place by being available, not by being equivalent. When the local agent does work, every gate stays the same: tests, reviews, verifiers.
Three more rules make it usable:
- Auto-approval, like the other two. Claude runs in bypass mode and Codex with its bypass flag. The local agent auto-approves every permission that is not explicitly denied, in the terminal UI and headless. A global check fails if that is ever lost.
- Model requests stay on the machine. Every remote provider is denied by policy, the endpoint must be a loopback address, and the check fails otherwise. Model downloads are separate: only the model manager fetches files. The agent's shell keeps normal network access, like any terminal. This is a routing boundary, not a sandbox.
- Local-only means local-only. In local-only work, a second opinion goes to a fresh local reviewer, never to Claude or Codex, and nothing is handed to them unless I ask.
Lock it to the machine
| 🚀 | Build your own, from scratch 1. Install the two halves. The agent and the model server are separate programs: 2. Start a model on your machine. 3. Point OpenCode at it. In 4. Give it a task with a visible finish. In a scratch folder run 5. Make it unattended. |
OpenCode can talk to many cloud providers. To make it a local agent, give it exactly one provider and deny the rest. A minimal ~/.config/opencode/opencode.json for OpenCode V2:
{
"$schema": "https://opencode.ai/config.json",
"providers": {
"local": {
"name": "Local models",
"package": "@opencode/ai/providers/openai-compatible",
"settings": { "baseURL": "http://127.0.0.1:8790/v1" },
"compatibility": { "reasoningField": "reasoning_content" },
"models": {
"coder": {
"modelID": "coder",
"capabilities": { "tools": true, "input": ["text"], "output": ["text"] },
"limit": { "context": 65536, "output": 16384 }
}
}
}
},
"policies": [
{ "action": "provider.use", "resource": "*", "effect": "deny" },
{ "action": "provider.use", "resource": "local", "effect": "allow" }
],
"model": "local/coder",
"update": "disable",
"share": "disabled"
}
The two policies restrict which provider OpenCode may use: deny every provider, then allow local. I checked this file in an empty home directory: opencode models listed one model, local/coder. Without the two policies, the same file listed 13. reasoningField tells OpenCode where llama.cpp puts the model's thinking, and limit must match what the server actually holds. On my machine a model manager writes this file from the lineup, with six purpose models and a variant that switches thinking on and off.
The policy is not the whole lock
The policies stop OpenCode from choosing another provider. They do not stop a project from re-pointing the allowed one: a project's own opencode.json can change the address of local, and the policy is still satisfied. So every model request on my machine also passes a guard, a small plugin that checks the provider, the model alias and the exact address at the moment the request is sent, and refuses anything else before a byte leaves.
Then Codex reviewed it, four rounds, and took it apart. A project can also switch plugins off, guard included. The launchers inherited variables that could load another configuration. A remote server could be attached with one flag. The test that proved the guard could end up checking a different binary from the one it launched. The fixes: one launch path for every way I start the agent, a private server for each launch, and a second copy of the guard that the launcher loads after every project setting, which survived every targeted project override I tested (a project that removes every plugin stops OpenCode before it sends anything).
I then attacked it the way a hostile repository would: a project that removes both guards by name and points local at a stand-in for a remote server, once at startup and once by reloading the configuration of a running server. With the second copy, the stand-in received nothing. Without it, it received two requests each time. That test now runs by itself, once for every new OpenCode version, before the agent is allowed to start, and takes a few seconds.
Inside the machine
The machine is a MacBook Pro with an M4 Max: 16 CPU cores, 40 GPU cores and 128 GB of unified memory, running macOS 27.0.1. Unified means the CPU and GPU share it. It does not mean all 128 GB hold model weights. Metal reports 110,100 MiB (about 107.5 GiB) usable by the GPU without raising the wired-memory limit, and I did not raise it. The operating system, the apps, the context cache, compute buffers, the vision encoder and speculative-decoding heads all need room too.
Dense, sparse, quantized
A dense model uses every parameter for every token. Qwen3.8-27B is dense: 27 billion parameters, all of them read for each token.
A mixture-of-experts model has many expert blocks and a router that picks a few per token. Qwen3.6-35B-A3B holds 35 billion parameters but uses about 3 billion per token. That cuts computation, not the download: every expert still has to sit in memory, ready.
Quantization stores weights in fewer bits. A label like UD-Q6_K_XL is a recipe, not a promise that every weight uses exactly six bits: recipes keep sensitive tensors at higher precision. You trade some fidelity for a smaller footprint and more speed.
The lineup
The names are the trick. OpenCode asks for local/coder, never for a file. A model manager re-points the name when a newer model passes its tests and scores at least as well, the same way a cloud provider re-points a model alias. Purpose aliases let instructions survive model replacements.
Flash-Next is the candidate worth the trouble. Artificial Analysis gives it an Intelligence Index of 40 (Qwen3.8-27B at xhigh effort: 34; Opus 5.5 at max: 58). It is a 125-billion-parameter mixture of experts with about 6 billion active, plus a 51-billion-parameter n-gram embedding table. The build I tested, UD-IQ4_XS, is 93.7 GB of files. A llama.cpp change merged on 13 October (PR #30405, shipped in 0.7.0) stops pinning that table in GPU memory and reads it from disk on demand, which turns "barely fits" into "might fit with room". Its license is the Qwen Community License, with conditions for some commercial services, not Apache-2.0.
So I loaded it, alone, with the router stopped. It loaded in 32 seconds and wired about 75 GiB. In the same hot state, right after a long evaluation of the 27B, it wrote 27 to 30 tokens per second against the 27B's 10 to 12, and read long prompts at about 350 tokens per second against 113 to 117. On the acceptance suite it matched the 27B on tools and long-context retrieval. On coding it passed 5 of 6 tasks against the 27B's 4, under different request caps: 1,800 seconds for Flash-Next, 900 for the 27B, whose two misses were timeouts while it was still thinking. Flash-Next used up its whole budget twice, on one coding answer at 20,480 tokens and one refusal probe at 17,408, the probe without writing any answer text.
The coder role requires every answer to finish, so Flash-Next failed that gate. The 27B fails it too: re-run on 19 October with time limits scaled to the token budget, it passed one of its two timed-out tasks, for 5 of 6, and used up the same 20,480-token budget on the other, the task Flash-Next ran out on. That gate does not separate them; three other findings do. At about 75 GiB, Flash-Next does not fit beside the fast model within the 94 GiB the model manager budgets for two models at once, and during its evaluation swap grew from about 1 MiB to almost 13 GiB with no other model loaded. It ran with one request slot, the conservative setting for multi-token prediction, so the agent's requests and its title requests would queue behind each other; two slots are unmeasured. And OpenCode with the 27B passed 77 of 77 hidden tests in the agent bake-off. Measured, not adopted; the faster brain stays on the shelf until a build fits beside the fast model.
One server, many names
The runtime is llama.cpp's llama-server in router mode: one router on 127.0.0.1 that serves every alias and starts a worker process for each model on demand. It parses Qwen's XML tool calls and its reasoning on the server side, so the agent receives clean tool calls. It supports multi-token prediction for faster generation and caches prompts. A launch agent keeps it running, and it runs offline: only the model manager downloads anything.
Why prefill decides how an agent feels
A chat demo shows generation speed: how fast words appear. Without a working prompt cache, re-reading a growing conversation can dominate an agent's runtime. Before every step it reads its instructions, the conversation, the files and the tool results. That is prefill, and nothing new appears until it finishes.
A useful approximation:
step time ≈ uncached input tokens ÷ prefill rate
+ generated tokens ÷ generation rate
+ tool timeMeasured on this Mac:
The router that serves local/fast measured far below the side-port runs of the same model, and I have not explained that gap yet.
At 200 tokens per second, 20,000 uncached tokens take 100 seconds. At 1,000, they take 20. An agent reaches 20,000 tokens of context quickly, so a dense model that re-reads everything on every step feels broken even when its generation speed looks fine.
The rescue is the prompt cache. A stable prefix is processed once; each step adds only the new suffix. With 97 % of its input served from the cache, the duel's local run below was mostly generation: about 40,000 output tokens at a median of 16 tokens per second.
The cache bug was in my own plugin
The Qwen 3.x models here are hybrids: three of every four layers are recurrent (Gated DeltaNet), not attention. A recurrent state cannot be rewound to an arbitrary point, so the server resumes only from checkpoints it saved near the end of the previous prompt; a Qwen Code bug report describes them at 5 and 513 tokens before the end. With that checkpoint behavior, an edit before the earliest retained checkpoint forces the server to process the whole conversation again. On a pure-attention model the same edit costs only the text after it.
My harness plugin did exactly that. It delivered harness notes (parallel sessions, limit warnings) by appending them to the system prompt in the middle of a turn. The fix was small: notes now go at the end of the conversation, inside the newest tool result or the next prompt, and stay in history unchanged. Prefix stability is a performance feature, and it belongs in the latency investigation next to every GPU setting.
Speculation helps, sometimes
Multi-token prediction drafts a few tokens with a small head and lets the main model verify them in one pass. With the separate draft head, the served 27B reached about 28 tokens per second on short prompts, and acceptance was 39–44 % at the model's recommended temperature of 1.0. llama-bench without speculation measured about 16, but that is a different harness, so the gain is indicative, not isolated. At 9.7k tokens of context the served speed falls to 18.7. "Supports speculation" is not evidence that a given configuration helps; measure it.
Measuring a laptop honestly
The same 27B file measured 7.6 tokens per second in one llama-bench window and 16 four minutes later, while the router's copy of the model stayed loaded. I did not isolate the cause. The slowdown also cost me a conclusion. The head built into the model file measured 14.7 tokens per second against 28 for the separate head, and I first wrote it down as slower. Then I checked the timestamps: a plain served run without speculation from the same ten minutes measured 7.4, half of what llama-bench measured without speculation (15.6–16.0). Every run in that stretch was slow, so the built-in head is not proven slower; the separate head simply won the runs that were not in it. Comparisons should use repeated, interleaved runs; that re-measurement is still pending here, and the tables show ranges.
I also measured lower throughput after a long evaluation, without isolating the cause. At about 9,700 tokens of context the 27B read 196 tokens per second and wrote 18.7 in the fast runs of 18 October. Right after a 71-minute evaluation, the same model through the same server read 117 and wrote 11.9.
What "uncensored" means here
In 2024, Arditi and colleagues showed that in each of the chat models they studied, refusal is mediated by a single direction in the residual stream, the running vector each layer reads and writes ("Refusal in Language Models Is Mediated by a Single Direction", arXiv:2406.11717). Erasing that direction kept the models from refusing harmful instructions; adding it made them refuse even harmless ones. Removing a direction is a projection:
h′ = h − r (rᵀ h) r: the unit refusal direction, h: an activationAbliteration applies that projection to the weights, so the model can no longer write along the refusal direction. Heretic, an open-source tool by p-e-w, automates it and tunes the cut with an optimizer that minimizes refusals and KL divergence from the original model together. KL divergence measures how far the new model's output distribution drifts from the old one on ordinary prompts: low KL means the model still behaves like itself.
Low KL is evidence, not a warranty. It does not prove equal coding ability or tool reliability. So acceptance here is two gates. A refusal probe of six ordinary requests that over-cautious models refuse checks the refusals; nothing harmful is requested or stored. The normal coding and tool suites check that nothing else moved.
The probe then taught me its own limit. Given room to think, the stock 27B answered all six as well, so the probe cannot tell a stock model from a decensored one; it only catches a build that has become over-cautious or broken. The publisher reports fewer refusals on harmful prompts; this setup does not reproduce that measurement, on purpose.
The publishers' own numbers:
- llmfan46, Qwen3.8-27B Heretic: KL 0.0244 · refusals 3/100 (base 91/100)
- JonathanColetti, Qwen3.8-27B uncensored: KL 0.119 · refusals 12/100 (base 98/100)
The two publishers measured on their own prompt sets, so the two lines are not a ranking. For Flash-Next there is a third route: AtomicChat publishes a 318 MB rank-1 adapter that the server can switch on per request. Its own card says that thinking at effort xhigh "is not production-ready on this build", and the harness runs every model at its maximum effort.
The llmfan46 build passed. On a private copy of the server it scored 6 of 6 on tool calls, coding and the refusal probe, found the planted fact and finished every answer, and it now serves local/uncensored; the stock 27B stays as the rollback. Two cautions remain. The probe cannot show the decensoring itself, as above; what the run shows is that the build kept its tools, code and long-context abilities. And the publisher's figures come from a card whose files carry an unexplained "Ultra" in their names; a Hugging Face redirect suggests a repository rename, but the card does not confirm it.
It is for my own lawful work on my own machine, and it changes no harness rule, permission, repository boundary or review gate.
"Always latest", defined
I wanted the local models to keep improving the way cloud models do, without editing a single filename. The definition that makes this testable: evaluate newer candidates, and replace the incumbent only when a candidate clears the role's acceptance rule.
The model guide at the top lists the nightly steps. A failure leaves the running models untouched and is remembered for that role, so the same build is not proposed again; a wrong rejection, like the one an infrastructure error caused during this build, can be lifted with its reason kept. ai-local promote ROLE MODEL runs the same staged path by hand. Unattended promotion was switched on only after that path had passed once for real, on the uncensored lane.
The acceptance suite is small on purpose: six tool calls with exact arguments, six coding tasks with hidden unit tests, the refusal probe, a fact planted in a 32,000-token document, and speed. It caught its own first bug. Thinking models got 700 output tokens and returned four empty answers that looked like refusals; the receipt could not show why, but with a larger allowance the same model answered all six. They now get a 16,384-token thinking allowance, and every case records why it stopped. An independent audit added the next lesson: a weighted average can hide a missing capability, so each capability now has its own gate. For the coder role, tool calls and long-context retrieval must be perfect, two thirds of the coding tasks must pass, and every answer must finish. Code the model writes runs in a macOS sandbox with no network and no writes outside its folder.
One harness, three agents
In "The Harness" I described one rulebook for Claude and Codex. Adding a third agent tested whether that design generalizes.
One policy, three native files. The canonical policy generates CLAUDE.md for Claude and AGENTS.md for Codex and OpenCode, plus each agent's skills. OpenCode V2 does not read CLAUDE.md (V1 used it as a fallback), so generating the right native file matters more than hoping a convention carries over. A global check fails on drift.
Hooks through a plugin. OpenCode has no shell hooks, so a small plugin forwards its events to the same harness program the other agents use. Before a tool runs it applies the shared-checkout guard (another session's uncommitted files) and the knowledge-base search guard; a denial rejects the call with the reason. It also forwards tool results, prompts and session start, stop, failure and end. It fails open and logs every failure, like the other agents' optional hooks.
Takeover transfers evidence. At 95 % a LIMIT WATCH tells the active agent to finish the step, push, and write a record: the task, verified work, next steps and open risks. The next agent reads it and re-derives the rest from the repository. The record transfers evidence and unfinished work; it cannot transfer an invisible mental state.
Second opinions follow the route. A Claude author is reviewed by Codex and the reverse. A local author is reviewed by Claude, then Codex, then a fresh local reviewer, and local-only work only by the local reviewer. A local reviewer is another attempt, never cross-provider agreement.
The loop has an ending. This whole build ran inside a Ralph loop: the request written down verbatim, a plan, and a strict verifier that decides "done". An interactive session keeps working while the verifier fails, and a launch agent runs fresh headless iterations, falling back from Claude to Codex, and to the local agent only when both are held. That order belongs to this loop; the harness's other loops fail over between Claude and Codex only. When Claude's five-hour window passed 95 % just before 16:00, the driver saw the warning and started a headless Codex iteration within seconds. Codex took over the bake-off while Claude checkpointed, and the two coordinated through short messages. After the reset, a fresh Claude iteration picked up the next items. That was a handoff between the two frontier agents; whether the local agent can carry a whole project alone is what the duel below measures.
The agent bake-off: same model, different agents
OpenCode was not a given. The research compared the open-source coding agents that can run on local models, and an independent audit checked it. Three made the shortlist for a hands-on test with the same model:
- OpenCode 2.0.32 (MIT): integrated, with a provider policy that can lock it to the machine and a plugin API that can reject a tool call.
- Pi 1.3.0 (MIT): a small agent with native llama.cpp support and a default first request of about 8,400 characters (roughly 2,100 tokens). In a Hugging Face blog post of 15 June 2026, Thomas Kim ran Terminal-Bench 2.0 with a 4-bit Qwen3.6-27B: with Pi on llama.cpp it solved 38 of 89 tasks (42.70 %), with Codex CLI 31 (34.83 %). For the Codex number, Kim writes: "After retrials and some manual stitching, I obtained the following unofficial baseline". One run each, not a ranking.
- Qwen Code 0.25.2 (Apache-2.0): Claude-Code-compatible hooks; telemetry and auto-update are on by default and have to be switched off.
Codex CLI was protocol-gated: its custom providers need OpenAI's Responses API behavior. Claude Code pointed at a local endpoint works technically but conflicts with the harness's credential rules. Goose is the next full candidate. A few others were excluded for maintenance or license reasons, which says nothing about the code they write.
The test: three small tasks, each with hidden tests the agent never sees. One is a bug hunt in a money-formatting package, one adds features to a command-line tool, one implements SemVer parsing and ranges from a specification. Each agent got the same prompt, the same model and full auto-approval, ran headless, and the hidden tests ran afterwards.
Every agent solved the two easy tasks. The SemVer task, which rewards reading a specification carefully, separated them: OpenCode passed all 43 hidden tests, Qwen Code missed one invalid range (1.2.3 ||), and Pi accepted the same range and built the same comparator for > and >=. Pi was about twice as fast on the easy tasks. OpenCode ran with the plugin version that still added pending harness notes to the system text during a turn, the cache bug above; this batch did not measure its effect on OpenCode's times.
The review after the tests found what the tests missed. OpenCode's ledger accepts a newline before the decimal point, which three extra probes caught; Pi left a scratch database in its repository. Hidden tests are evidence, not proof of correctness, and one attempt per agent is not a ranking.
No challenger won clearly, so OpenCode stays: it was the only one to pass all 43 hidden SemVer tests, and it is the one already wired into the harness.
The duel: Claude against the local agent
The duel changes everything at once: model and agent. Both got the same small Python project, an event-window module, a router and a command-line interface written to a specification, with the same prompt, run headless with full permissions, and the same 48 hidden acceptance tests afterwards.
Both passed all 48 hidden tests. That is the headline: a free model on a laptop built the same project to the same specification, unattended, and committed its work after 43 minutes, Claude after 26. It took 30 steps and 39 tool calls, hit one tool error (a doubled path segment) and fixed it itself. The prompt cache earned its keep: 97 % of the local agent's 1.33 million input tokens were read from it.
Reading the two solutions shows where the difference went instead. Both found the planted bug, the same one-character boundary error, and both wrote a notes file with their design decisions and interpretations. Claude's version is more careful at the edges. It validates the configuration: a window with a missing or zero capacity fails with a message and exit code 2, where the local version crashes with a traceback on the first and prints "percent": Infinity, which is not valid JSON, on the second. It sums with math.fsum and multiplies before dividing, because 29 / 100 * 100 is 28.999999999999996 in floating point and can land on the wrong side of a threshold; the local version divides first. Claude finds the reset time by bisection; the local version tries every candidate instant and recomputes the window each time, which is fine at this size. On one ambiguous rule, whether events stamped after "now" count when computing the reset time, the two chose differently, and the hidden tests do not decide it. Claude wrote 193 tests and checked them by reintroducing the original bug and 31 other deliberate faults, all caught; the local agent wrote 76.
A Claude-authored review found no defect in either on the points the specification defines. The reviewer shares a model family with one contestant, so the hidden tests remain the primary score. What separates the two is robustness and how each read the rules the specification leaves open, and that is why the local agent keeps every gate when it works.
Forty-eight passing tests show success against those tests, not general parity. The public boards put the gap in perspective. On Vals' Terminal-Bench 2.1, Qwen3.8-27B scores 58.43 %, Opus 5.5 87.64 % and GPT-6 Astra 87.27 %. On Vals' Vibe Code Bench, Qwen3.8-27B scores 64.85 % and Opus 5.5 90.29 %. Qwen's own model card reports 73.0 on Terminal-Bench 2.1 with its own setup; a vendor's number and an independent board are different measurements, and neither measured my quantized copy.
Steal this
- Name purposes, not models. Point your agents at
coderandfast, and let a manager re-point the names after a test. - Measure prefill and cache reuse, not just tokens per second. Without cache reuse, prefill can dominate runtime.
- Keep the prefix stable. Append; never edit the beginning of a conversation mid-session, especially on hybrid models.
- Gate replacements. A new upload is a candidate, not an upgrade: the same tests run before the name moves.
- Guard the request, not only the config. A policy that names providers does not stop a project from re-pointing the allowed one. Check the address when the request is sent, and attack your own guard before you trust it.
- Give the fallback the same gates. Availability never justifies a lower bar.
- Write the handover to disk. The next agent should need the record and the repository, nothing else.
Honest limits
- The local models are weaker on hard problems, by a wide margin on fresh agentic benchmarks.
- The lineup is the 27B for coding and the 35B for fast reading. Flash-Next is faster but does not fit the memory budget beside the fast model for now; it and the 27B both miss the completion gate on the same coding task. The uncensored lane runs a decensored 27B that passed every gate, but its advantage on harmful prompts is the publisher's measurement, not mine.
- Laptop speed varies, and the acceptance suite covers a narrow slice of real work.
- The harness plugin fails open: if the harness is down, its checkout and search guards cannot block. The request guard fails closed, and it covers model requests only; the agent's shell keeps the network, like any terminal.
- The local agent is a fallback and a private workshop, not a replacement.
Its most useful property is that it is there when the other two are not.