We’ve been using DeepSeek V4 Pro as our daily-driver coding model for a few months now, through a Go-based terminal harness we built called cwcode. Not for benchmarks. For actual work: training dose-prediction models for radiotherapy, building a financial research agent, writing the harness’s own code.
DeepSeek V4 Pro charges $0.435 per million input tokens, $0.003625 on cache hits, $0.87 on output. Claude Sonnet 4 sits around $3 / $0.30 / $15. Call it 5–7× cheaper across the board, with the cache spread even wider. If you trust the headline numbers on coding benchmarks, V4 Pro lands somewhere around 80–85% of Claude on long tasks. We’d put it at 90% in our actual workflow, but we had to build a lot of the gap-closing into the harness ourselves. None of the off-the-shelf agents we tried get there on V4 Pro.
This is the writeup of what it took. Where the gap is real, what closed it, and what we still can’t fix without a better model.
A few things V4 Pro just doesn’t do as well as Claude, and we don’t think harness work can fix:
- Long-horizon planning over unfamiliar code. Drop V4 Pro into a 50k-line codebase it’s never seen and ask it to refactor an architecture, and it’ll happily make four reasonable-looking edits that collectively don’t compile. Claude is noticeably better at holding the whole picture. We handle this with explicit Plan mode and keeping turns short.
- Reading sloppy code. Claude is more forgiving of weird naming, dead branches, and undocumented invariants. V4 Pro wants the code to make sense, and when it doesn’t, the model invents a sensible-looking version of what should be there.
- First-shot UI work. Claude’s first attempt at a React component is usually closer to “ready to ship.” V4 Pro is closer to “ready to iterate on.” For us, that’s fine; for someone using an agent to scaffold consumer apps, probably not.
- Following a precise spec. Give it `here’s the file, change line 47 to do X`, it does X. Faster than Claude, and at 5% the cost we don’t care about a few retries.
- Numerical and scientific code. This one surprised us. On our PyTorch training loops and Monte Carlo simulation glue, V4 Pro’s first attempts are noticeably more correct than Claude’s. We suspect the training mix.
- Bash and ops glue. Dead even.
So the harness’s job is to lean into V4 Pro’s strengths (precise execution against a clear spec) and structurally compensate for its weaknesses (planning, ambiguity tolerance). Most of what follows is one of those two things.
In February, Can Akay published [a post on coding-agent edit-tool design](https://blog.can.ac/2026/02/12/the-harness-problem/) that we think is the most important coding-agent paper of the year. Akay’s claim: most agent failures aren’t model failures, they’re harness failures, and specifically the harness’s edit tool. Asking a model to reproduce file content character-perfect — which is what `old_string` / `new_string` and patch-format both require — burns tokens on retries and makes weaker models look much worse than they are.
His proposed fix is what he calls “hashlines.” Annotate every line you show the model with a short content hash. Let the model edit by reference, not reproduction. Akay showed Grok Code Fast jumping from 6.7% to 68.3% on SWE-bench Verified just from this format change. Output tokens dropped 61% because the model wasn’t generating the same `old_string` block three times to land one edit.
We implemented this two weeks ago. Our `read_file` tool now returns:
1:5c2| package tools
2:a1f|
3:0eb| import “os”
4:dbd| import “strings”
Three hex chars per line, hash of the trailing-whitespace-trimmed content. The new `edit_lines` tool takes line ranges with the expected hashes at each endpoint:
{
“path”: “internal/tools/read.go”,
“edits”: [{
“from”: 3, “from_hash”: “0eb”,
“to”: 3, “to_hash”: “0eb”,
“new_text”: “import \”fmt\”“
}]
}
The harness reads the file fresh, recomputes hashes, and rejects the entire batch on any mismatch with a precise error:
edit_lines: line 3 hash mismatch — claimed “0eb”, actual “0e1”.
Current line: “import \”strings\”“
We didn’t deprecate the old exact-string `edit_file`. Sometimes the model wants to edit without doing a fresh read, and that’s fine. But `edit_lines` is the preferred path now, and the failure rate on the first attempt dropped sharply. On V4 Pro our internal numbers show roughly half the retries per task and 30–40% lower output tokens per session. Not a Grok-sized jump — V4 Pro was already better at exact-string matching than Grok — but enough that an autonomous loop now feels different to use. It doesn’t keep getting stuck.
If you only do one thing in this post, do this one. The hashline pattern is provider-agnostic and cheap to implement (~200 lines of Go).
DeepSeek’s prompt cache is keyed on exact byte prefix. Match a previous request’s first N tokens and those tokens bill at roughly 1/120th the price. Match nothing and you pay full miss rate.
Three things commonly destroy the prefix:
1. Timestamps in the system prompt. “Current time is 14:23:11” changes every turn. Cache hit ratio: 0%.
2. Re-sending reasoning_content. DeepSeek’s docs explicitly say not to. We strip it before every outbound request, on every provider, by default. (Kimi-for-Coding is the only model we know that *requires* preserved reasoning; for that we expose an opt-in flag.)
3. Non-deterministic tool serialization. Map iteration order in Go is random. If you build your tool schema dict from a map without sorting, your tools list changes between requests and the prefix breaks.
Our `/cache` slash command shows session-cumulative hit ratio. After the third or fourth turn of any session we routinely see 85%+. A four-hour autonomous loop that does 50 turns of real work costs us $0.40 to $0.80. Most of that is output tokens; almost none is missed-cache input.
If you’re paying for input tokens at the full rate every turn, your harness is leaving real money on the table — and on V4 Pro, “real money” means most of your bill.
Every coding agent eventually hits a tool-call loop where the model keeps making the same broken call. Standard defense: detect three consecutive identical failures and stop the turn. Most implementations stop with a red error and leave you staring at it.
Ours synthesizes a response. When the loop trips, we inject an assistant-role message into the conversation:
> I’m unable to continue: tool `read_file` failed 3 times in a row with the same error: `’path’ argument is empty or missing`. This usually means the arguments are wrong, or the target doesn’t exist. Please clarify what you’d like me to do, or check the inputs and try again.
It streams through the normal content path. The user sees a coherent message in the assistant’s voice. The synthesized turn lands in `Messages`, so when the user clarifies, the model sees its own “I gave up because X” and replies sensibly.
We pair this with making the underlying tool errors actually useful. `os.Open(”“)` returns “open : no such file or directory” — which doesn’t tell you the path was empty. We catch that explicitly and say so. `read_file` failures wrap the path. `edit_file` failures include line numbers for partial matches. Most former storm-breaker triggers now self-resolve because the model gets enough information to fix its own call.
Two features make us comfortable running multi-hour autonomous loops on production code.
Plan mode restricts the agent to read-only tools. The LLM doesn’t see the mode flag — it just sees a different (smaller) tool registry, with a system prompt addendum telling it to produce a numbered plan instead of changes. Shift+Tab toggles. By default the human picks the mode; in YOLO mode we expose a synthetic `set_mode` tool the LLM can call to self-transition.
Rewind is a content-addressed blob store. Before any file-mutating tool runs, the harness snapshots every path the tool declares it’ll touch (the tool’s `MutatedPaths()` method). Snapshots are SHA-256-keyed and live under `~/.cwcode/sessions/<id>/objects/`. A `checkpoints.json` records turn order, the user’s prompt that started each turn, and the pre-state hashes.
`/rewind N` restores every file to its earliest captured pre-state across turns ≥ N, truncates conversation history, and pre-fills the input box with the original prompt so the user can edit and re-submit. A hash of `”“` is the sentinel for “didn’t exist before this turn” — on restore the file is deleted.
The combination is what makes autonomous mode tolerable. You hand the agent a refactor in Plan mode, read what it proposes, Shift+Tab to commit, watch it execute. If it does something dumb, `/rewind 3` puts you back where you started and you re-prompt with better framing. Worst case is a few minutes and maybe a buck.
cwcode is about 12k lines of Go and uses Bubbletea for the TUI. The structural decision worth lifting is a `Sink` interface that decouples the agent loop from rendering. The agent never writes to stdout or reads from stdin directly — it emits events and asks for permissions through `Sink`. Two implementations: a plain stdout sink for `-p` one-shot and CI runs, and a TUI sink that bridges into Bubbletea. If you wanted to put this behind a web UI or a Slack bot, you’d implement the sink, not touch the agent.
Most of the rest is plumbing that took longer than it should have. An index-keyed streaming tool-call accumulator that correctly handles llama-server’s habit of only sending tool name and ID in the first delta of a streamed call. A permission cache keyed on stable hashes of tool arguments. A two-pass auto-compactor that first elides old tool outputs, then summarizes prior history if still over threshold. Bracketed-paste-safe input handling. None of this is glamorous; all of it is what makes the agent usable for hours at a time.
Dose-prediction model training and inference. Our day job is deep-learning models that predict 3D radiation dose distributions for cancer radiotherapy planning. The codebase is PyTorch training loops, Monte Carlo simulation interfaces, DICOM-RT plumbing. Every edit has correctness implications — a transposed axis in a dose accumulator silently produces unsafe output. Plan mode for any tensor-shape change is non-negotiable; Rewind catches the regressions Plan mode misses. We use the agent to write training scripts, debug NaN cascades, port older models to new datasets, and stand up inference servers.
A financial research agent. Reads filings, market data, and internal model outputs; produces analysis. Regulated domain, so every tool call is logged with timestamps and pre/post hashes via the same checkpoint store. We build the financial agent in cwcode and the financial agent itself uses cwcode-style tools internally. The harness pattern composes.
cwcode itself. This is the loop we like best. The harness ships its own bugs, we type the bugs into the running TUI, most get fixed in the next turn or two. We shipped 11 versions in one day last week. Half the commits are the agent’s work. The other half are bug fixes for things the agent did wrong, also written by the agent.
Some bugs we shipped and then fixed in the same day:
Bubbletea fragments SGR mouse-event sequences in some terminals. The tail bytes (`;42;58M`) come back as a separate KeyMsg with printable runes, and we were inserting them into the textarea on every wheel scroll. We added an 80ms post-mouse grace window that drops `KeyMsg.Runes` arriving right after a `MouseMsg`. Fixed.
Our slash-command popup hijacked `j` and `k` as vim-style up/down aliases, which made `/joke` and `/kill` literally untypeable. Worse, pressing Enter with the popup open submitted the matched *command name* instead of the user’s full input — so `/goal write a parser` got reduced to bare `/goal` and just printed the status output. Two separate bugs, found in the same hour.
The storm-breaker used to silently abort with a red error. Users assumed the agent was broken. Now it synthesizes a response.
`preserve_thinking` used to default to “keep” on non-DeepSeek providers. Local vLLM users were watching their context windows fill up with stale reasoning across turns. Default flipped to “strip” universally.
This is what running your own harness looks like day to day. You find friction by experiencing it, you fix it in five minutes, the build lands in `~/.local/bin/cwcode` ninety seconds later, you go back to whatever you were doing. The feedback loop is the whole reason we built this in the first place.
V4 Pro is good enough that the harness is now where the differentiation happens. If you’re getting Claude-quality output from V4 Pro and paying 5% of the price, your harness is doing 80% of the work that would otherwise be your model bill.
Specifically:
- If you do nothing else, replace exact-string editing with hash-anchored editing. The pattern is in Akay’s post and our implementation lives in `internal/tools/edit_lines.go` if you want to crib it.
- Make your system prompt byte-stable. Sort your tools. Strip reasoning_content from outbound. The DeepSeek cache will reward you 120×.
- Don’t silently abort. When the loop breaks, *say something*.
- Plan mode and Rewind aren’t features, they’re the precondition for using an autonomous agent on real code.
- Use your harness for real work. Every bug you don’t experience daily is a bug your users will find.
V4 Pro isn’t the future-of-coding model. But it’s plenty good as a daily driver if you meet it halfway. The teams that figure out harness design now will compound advantage as the model landscape commoditizes.
---
cwcode is currently at My Google Drive and feel free to try and let me know your experiences.
