Hacker News
July 20, 202610 min read
If you don’t know what OpenCode is, imagine a boot stamping on a human face forever. The boot is made of TypeScript and the face is everything we have learned about security and systems software since the invention of the electronic computer in the 1940s. The creators describe it as an AI coding agent. As far as I can tell it’s the most popular open-source coding agent, and it currently has 161k stars on GitHub.
I’ve tried out OpenCode with a local LLM. My conclusion is that OpenCode is clown-car turboslop with a security posture of “let me bend over for you daddy”. Everyone using it should stop using it.
There are two parts to this post: annoying things and alarming things . The second part is longer. I wrote this post with reference to source code from OpenCode git version baef5cd4 .
I don’t consider anything in this post to be a security disclosure. OpenCode is fundamentally a web-stack tool for piping llm | bash , and all the issues I describe are in the “pipe” part. The ways it fails are fascinating in the fractal nature of the poor decision making, but the outcome was foregone.
I tried to keep discussion of LLM use separate from whether everyone using LLMs should have their machines trivially exploited or accidentally wiped. There is a post-script with some brief thoughts on local LLMs.
Let’s put security to one side for a moment and examine how OpenCode fails as a tool even when it’s not causing you to get your shit popped. There is a kind of Bethesda Effect with OpenCode where it’s impossible to tell what is a bug and what is by design, so I stuck with a description of “annoying”.
Most local LLM servers use some variant of the OpenAI /v1/chat/completions API. The idea is:
You POST a JSON blob with the entire conversation so far.
You get back a stream of SSE events which add up to the response.
The upload cost over a session is quadratic, and download is amplified by wrapping tiny deltas in JSON with repeated metadata. Tool calls use the elusive “double JSON encoding” so they can be serialised as multiple JSON-encoded deltas that reassemble into more JSON.
The setup has one benefit, which is the server is stateless. As usual, the way you make stateless things fast is: state . The server caches evaluations. When it receives a request, it:
Finds the longest matching cached prefix.
Evaluates from end of prefix to end of the last posted message (“prefill”).
Generates new tokens until it encounters an end-of-sequence token.
I used Qwen3.6-27B dense on an M4 Max, which has decent memory bandwidth (~0.5 TB/s, high for a CPU SoC, low for a GPU). Token generation is usable but it is extremely compute-bound in prefill. If my server can’t find a good matching prefix for the request prompt when I’m deep into the window then I might have to wait 10 minutes of max GPU usage for it to start generating a response. That’s fine, because this should happen rarely. Should.
Here are some of the ways OpenCode missed the memo on this one:
It globs your filesystem and re-reads AGENTS.md (injected in turn-0 system prompt) on every SSE turn. If you put a quick note in AGENTS.md to be read in the next session, you immediately force a full re-evaluation.
It prunes context from tool calls on every agent → user transition, invalidating a large part of the prefix.
Pruning just discards tool call results more than a fixed distance const PRUNE_PROTECT = 40_000 behind the write head. In the best case you’re taking a 40k context miss, which is equivalent to reading a full-length novel per two or three turns.
Agent → user transition includes interruption, so if you need to pull the clanker out of a rabbit hole and re-steer it, OpenCode immediately trashes the prompt cache and makes you wait for a response.
Personal favourite: it puts the current date in the turn-0 system prompt and re-evaluates every SSE turn. If you’re using OpenCode at midnight you get a full prompt cache miss.
These are the prompt cache misses that fit solely in this category. There are many more; I’ll call them out as we go.
I mentioned pruning in the previous section. The prompt cache misses aren’t worth it so I disabled it. The other glaring issue is the lack of protection for early reads. It might not be obvious how completely broken this is, so let’s work through an example. Suppose you start a fresh session, and tell your clanker to first read a spec or implementation plan, then write some code:
The clanker goes and reads related code, very likely putting it over the fixed 40k pruning threshold.
The clanker is ready to implement, but either immediately dives down a dumb rabbit hole, or sits in chain-of-thought dithering about something that is actually very simple or already well-specified.
You interrupt the clanker to re-steer it.
The interruption causes the entire spec to be deleted from the context window.
The clanker writes code without being able to refer to the spec.
Pruning applies equally to all results of all tools except for skill , which is never pruned.
Want to sit for 10 minutes while the LLM server prefills the entire session with a new prompt prefixed to it, just to turn it into 5 bullet points that go at the top of a new session? Me neither. I get what they are going for, but I’ve not seen it work well. Neither compaction nor pruning is implemented well, and they interact poorly.
If you want to summarise a session then the summarisation prompt should be injected at the end to avoid prefilling the entire session from scratch. The best method I’ve found is just an explicit handoff by telling the clanker to write out notes. It’s ugly but it works better than OpenCode’s compaction mechanism, and creates an on-disk artefact that I can edit or reuse in multiple sessions.
Compaction is a leaky abstraction that tries to make a finite context window look like an infinite one. It’s better to accept context windows and prompt caches as a first-class feature of clanker wrangling, and expose better primitives for managing them. Pi has an interesting approach here with session trees, which deliberately exploit the prompt cache.
OpenCode pastes a system prompt at the top of new context windows. Fine and normal, but:
The default system prompt is incredibly verbose. Ironically most of the word count is explaining to the LLM how to be concise.
The default system prompt is opinionated (fine) but it has shit opinions (not fine). It took me a while to figure out why my agent kept saying “Use ABSOLUTELY NO COMMENTS ” when dispatching subagents.
The Plan-to-Build handover is clumsy and often leaves me towards the end of a context window by the time everything is sufficiently elucidated. I’d rather write out notes with everything discussed, so I can edit it and then hand off to a fresh session. To which, see next point:
The system reminder for Plan mode tells the clanker it can’t write to any directories, but it’s actually allowed to write to a specific .opencode/plans directory. I have seen this fail both ways: writing to this directory unprompted, and refusing to write when explicitly instructed.
There is no way to modify the default system prompt globally; you have to copy it into every project.
If you only override the default prompt in Build mode, switching to Plan mode is a full prompt cache miss.
Different per-model prompts have wildly varying contents and quality. They’re all worth a good hate-scroll but Beast Mode (GPT-4, o1 and o3) is my favourite. Quote:
You CANNOT successfully complete this task without using Google to verify your understanding of third party packages and dependencies is up to date.
You cannot. It’s just impossible. We don’t know how. Definitely don’t just read the source code for the package.
When the clanker tries to access a file outside of the project directory, if it does so in a way that OpenCode manages to recognise with ad-hoc string parsing (oops that is the alarming things section), you receive a prompt asking you whether to grant permission. This halts execution until you respond.
The answers are: Yes / No / Always . Do you see a missing answer here? How about Never ?
The interaction with subagents here is particularly broken. If a subagent tries to access a script output in /tmp , and I say No , it kills the subagent and all of its context for its partially complete work is lost. So I have to say Yes and let it write to /tmp or whatever it’s trying to do.
The other issue is decision fatigue: if I keep getting asked “can I do this?” and the only response that leads to productivity is “yes” then I’m eventually going to nod through something dangerous. Human fallibility should not be load-bearing for something as basic as “don’t write outside this directory”.
This is feature number 0 for a coding agent. It’s broken.
If I send a message while SSE streaming is ongoing, it gets queued. Nice feature. However:
The semantics of when OpenCode decides to actually send the message are a little unclear; the code suggests it’s at the end of a tool call turn but I have also seen tool → CoT transitions without sending my queued message.
If I subsequently interrupt because I want the clanker to actually answer my question instead of navel gazing, the message is removed from the “queued” state and just goes into the message log. You interrupted to get the clanker to answer your message, but now you can’t send the message. You have to send a second message to start a new stream.
Undoing a message often fails to remove it from the message log.
The problems continue with subagents (i.e. agents spawned by an agent using a tool call RPC):
I can’t talk to subagents; if they go down a dumb rabbit hole I have a choice of killing them and losing their context, or helplessly watching them burn tokens.
I looked into this and apparently OpenCode used to have this feature but it’s just… gone?
You can @mention a subagent from the main agent’s chat window but this doesn’t seem to do anything useful. In particular it doesn’t interrupt.
If a subagent fails a tool call (e.g. Qwen putting tool calls in CoT) then it’s fatal and all the context up to that point is lost.
The ability to reuse subagents seems nice in theory but in practice the main use of subagents is to break tasks into smaller context windows. Having the agent sometimes decide to reuse the same subagent for an unrelated task defeats this.
I have never seen this be beneficial and often seen it cause prompt cache misses (there it is again!) due to switching between agents and subagents that both have huge contexts.
As a principle: it’s ok to have a richer interaction model from the human side, but clankers will do every possible dumb thing, so choices need to be minimised.
On the positive side, OpenCode’s subagent interactions led to one of the funniest GitHub issues I have ever had the pleasure of reading.
Read what's here, then head to the original whenever you're ready - never required.
Continue Reading on Hacker NewsMeasuring What Matters with Jules
Google Developers July 21, 2026