Annoying and alarming things about OpenCode
Annoying and alarming things about OpenCode
关于 OpenCode 的那些烦人且令人担忧的事
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. 如果你不知道 OpenCode 是什么,想象一下一只靴子永远踩在人脸上。这只靴子由 TypeScript 制成,而那张脸则是我们自 20 世纪 40 年代电子计算机发明以来,在安全和系统软件方面所学到的一切。
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. 开发者将其描述为一个 AI 编程代理。据我所知,它是目前最受欢迎的开源编程代理,在 GitHub 上拥有 16.1 万颗星。我用本地大模型(LLM)试用了 OpenCode。我的结论是:OpenCode 就是一堆像小丑车一样混乱的垃圾代码,其安全姿态简直就是“求求你快来黑我吧”。所有正在使用它的人都应该立即停止使用。
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. 这篇文章分为两部分:烦人的事和令人担忧的事。第二部分篇幅更长。我撰写本文时参考了 OpenCode git 版本 baef5cd4 的源代码。我不认为本文中的任何内容属于安全漏洞披露。OpenCode 本质上是一个用于将“大模型输出 | bash 执行”的 Web 栈工具,我所描述的所有问题都存在于这个“管道”部分。
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. 它失败的方式在决策失误的分形本质上令人着迷,但结果早已注定。我试图将对大模型的使用讨论与“使用大模型的人是否应该被轻易入侵或意外清空机器”的问题分开。文末附有一篇关于本地大模型的简短思考。
Annoying Things
烦人的事
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”. 让我们暂时把安全问题放在一边,看看 OpenCode 在没有导致你被黑的情况下,作为工具是如何失败的。OpenCode 有一种“贝塞斯达效应”(Bethesda Effect),即你根本分不清什么是 Bug,什么是设计使然,所以我只能用“烦人”来形容。
Prompt Cache Misses
提示词缓存未命中(Prompt Cache Misses)
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. 大多数本地大模型服务器使用 OpenAI /v1/chat/completions API 的某种变体。其逻辑是:你 POST 一个包含迄今为止整个对话的 JSON 数据块,然后收到一串 SSE 事件流,最终汇总成回复。在一个会话中,上传成本是二次方的,而下载成本则因在 JSON 中包裹微小的增量并重复元数据而被放大。
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. 工具调用使用了难以捉摸的“双重 JSON 编码”,以便它们可以被序列化为多个 JSON 编码的增量,并重新组装成更多的 JSON。这种设置有一个好处,即服务器是无状态的。像往常一样,让无状态事物变快的方法就是:引入状态。服务器会缓存评估结果。
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. 当服务器收到请求时,它会:找到最长的匹配缓存前缀;从前缀末尾评估到最后一条消息的末尾(即“预填充”);生成新 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. 我在 M4 Max 上使用了 Qwen3.6-27B dense 模型,其内存带宽尚可(约 0.5 TB/s,对 CPU SoC 来说很高,但对 GPU 来说很低)。Token 生成速度尚可,但在预填充阶段极其消耗算力。如果我在对话窗口很深时,服务器找不到好的匹配前缀,我可能需要等待 10 分钟的 GPU 满载运行才能开始生成回复。这没关系,因为这种情况应该很少发生。本应如此。
Here are some of the ways OpenCode missed the memo on this one: 以下是 OpenCode 在这方面完全没搞懂的一些表现:
- 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. 它会扫描你的文件系统,并在每次 SSE 轮询时重新读取 AGENTS.md(在第 0 轮系统提示词中注入)。如果你在 AGENTS.md 中写了条笔记想在下个会话读取,你立刻就会强制触发一次全量重新评估。
- 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. 在每次“代理 → 用户”转换时,它会修剪工具调用的上下文,导致大部分前缀失效。修剪操作只是丢弃了距离写入头超过固定距离(const PRUNE_PROTECT = 40,000)的工具调用结果。在最好的情况下,你也会损失 40k 的上下文,这相当于每两三轮对话就读完一本长篇小说。
- 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. “代理 → 用户”转换包括中断操作,所以如果你需要把这个“破烂玩意”从死胡同里拉出来并重新引导它,OpenCode 会立即丢弃提示词缓存,让你重新等待响应。
- 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. I think this tries to remediate the clanker tendency to believe the current date is the date of its training cutoff, and refuse to believe newer things exist. It’s still funny. 我最喜欢的一点:它把当前日期放在第 0 轮系统提示词中,并且在每次 SSE 轮询时重新评估。如果你在午夜使用 OpenCode,你会遇到一次完整的提示词缓存未命中。我想这是为了纠正模型倾向于认为当前日期是其训练截止日期,并拒绝相信新事物存在的毛病。但这依然很滑稽。
Pruning
修剪(Pruning)
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 spec is read into context. 规范被读入上下文。
- The clanker goes and reads related code, very likely putting it over the fixed 40k pruning threshold. 它去读取相关代码,很可能会超过 40k 的固定修剪阈值。
- 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. 修剪操作同样适用于所有工具的所有结果,除了“技能”(skill),它永远不会被修剪。
Compaction
压缩(Compaction)
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. 想坐等 10 分钟,看着大模型服务器用一个新的前缀提示词预填充整个会话,仅仅是为了把它变成新会话顶部的 5 个要点吗?我也不想。我明白他们的意图,但我没见过它能很好地工作。
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. 压缩和修剪都没有实现好,而且它们之间的交互也很糟糕。如果你想总结一个会话,总结提示词应该在最后注入,以避免从头开始预填充整个会话。我发现最好的方法就是明确地告诉模型写下笔记。这虽然笨拙,但比 OpenCode 的压缩机制好用,并且能在磁盘上生成一个我可以编辑或在多个会话中重复使用的产物。