{"slug": "stop-using-opencode", "title": "Stop Using OpenCode", "summary": "A security researcher warns users to stop using OpenCode, the most popular open-source AI coding agent with 161k GitHub stars, citing severe security flaws and poor design. The tool, described as fundamentally a web-stack tool for piping 'llm | bash', forces full prompt cache misses on every SSE turn by re-reading AGENTS.md and injecting the current date, causing up to 10-minute delays on local LLMs. The researcher concludes OpenCode has a 'security posture of let me bend over for you daddy' and that all issues stem from the pipe component.", "body_md": "If you don’t know what OpenCode is, imagine a boot stamping on a\nhuman face forever. The boot is made of TypeScript and the face is\neverything we have learned about security and systems software since the\ninvention of the electronic computer in the 1940s. The [creators](https://opencode.ai/) describe it as an AI coding\nagent. As far as I can tell it’s the most popular open-source coding\nagent, and it currently has 161k stars on GitHub.\n\nI’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.\n\nThere are two parts to this post: **annoying things**\nand **alarming things**. The second part is longer. I wrote\nthis post with reference to source code from OpenCode git version\n`baef5cd4`\n\n.\n\nI don’t consider anything in this post to be a security disclosure.\nOpenCode is fundamentally a web-stack tool for piping\n`llm | bash`\n\n, and all the issues I describe are in the “pipe”\npart. The *ways* it fails are fascinating in the fractal nature\nof the poor decision making, but the outcome was foregone.\n\nI 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.\n\nLet’s put security to one side for a moment and examine how OpenCode\nfails as a tool even when it’s *not* causing you to get your shit\npopped. There is a kind of Bethesda Effect with OpenCode where it’s\nimpossible to tell what is a bug and what is by design, so I stuck with\na description of “annoying”.\n\nMost local LLM servers use some variant of the OpenAI\n`/v1/chat/completions`\n\nAPI. The idea is:\n\nYou POST a JSON blob with the entire conversation so far.\n\nYou get back a stream of SSE events which add up to the response.\n\nThe 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.\n\nThe setup has one benefit, which is the server is stateless. As\nusual, the way you make stateless things fast is:\n**state**. The server caches evaluations. When it receives\na request, it:\n\nFinds the longest matching cached prefix.\n\nEvaluates from end of prefix to end of the last posted message (“prefill”).\n\nGenerates new tokens until it encounters an end-of-sequence token.\n\nI used Qwen3.6-27B dense on an M4 Max, which has decent memory\nbandwidth (~0.5 TB/s, high for a CPU SoC, low for a GPU). Token\ngeneration is usable but it is extremely compute-bound in prefill. If my\nserver can’t find a good matching prefix for the request prompt when I’m\ndeep into the window then I might have to wait 10 minutes of max GPU\nusage for it to start generating a response. That’s fine, because this\nshould happen rarely. *Should.*\n\nHere are some of the ways OpenCode missed the memo on this one:\n\nIt 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.\n\nIt prunes context from tool calls on every agent → user transition, invalidating a large part of the prefix.\n\nPruning just discards tool call results more than a fixed\ndistance `const PRUNE_PROTECT = 40_000`\n\nbehind the write\nhead. In the best case you’re taking a 40k context miss, which is\nequivalent to reading a full-length novel per two or three\nturns.\n\nAgent → 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.\n\nPersonal favourite: it puts the **current date** in\nthe turn-0 system prompt and re-evaluates every SSE turn. If you’re\nusing OpenCode at midnight you get a full prompt cache miss.\n\nThese are the prompt cache misses that fit solely in this category. There are many more; I’ll call them out as we go.\n\nI 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:\n\nThe spec is read into context.\n\nThe clanker goes and reads related code, very likely putting it over the fixed 40k pruning threshold.\n\nThe 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.\n\nYou interrupt the clanker to re-steer it.\n\nThe interruption causes the entire spec to be deleted from the context window.\n\nThe clanker writes code without being able to refer to the spec.\n\nPruning applies equally to all results of all tools except for\n`skill`\n\n, which is never pruned.\n\nWant 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.\n\nIf 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.\n\nCompaction 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.\n\nOpenCode pastes a system prompt at the top of new context windows. Fine and normal, but:\n\nThe default system prompt is incredibly verbose. Ironically most of the word count is explaining to the LLM how to be concise.\n\nThe default system prompt is opinionated (fine) but it has shit\nopinions (not fine). It took me a while to figure out why my agent kept\nsaying “Use **ABSOLUTELY NO COMMENTS**” when dispatching\nsubagents.\n\nThe 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:\n\nThe system reminder for Plan mode tells the clanker it can’t\nwrite to any directories, but it’s actually allowed to write to a\nspecific `.opencode/plans`\n\ndirectory. I have seen this fail\nboth ways: writing to this directory unprompted, and refusing to write\nwhen explicitly instructed.\n\nThere is no way to modify the default system prompt globally; you have to copy it into every project.\n\nIf you only override the default prompt in Build mode, switching to Plan mode is a full prompt cache miss.\n\nDifferent per-model prompts have wildly varying contents and quality.\nThey’re all worth a good hate-scroll but [Beast\nMode](https://github.com/anomalyco/opencode/blob/c5db39f6268a36194a7fe5f833ae3197dfe250b6/packages/opencode/src/session/prompt/beast.txt) (GPT-4, o1 and o3) is my favourite. Quote:\n\nYou CANNOT successfully complete this task without using Google to verify your understanding of third party packages and dependencies is up to date.\n\nYou cannot. It’s just impossible. We don’t know how. Definitely don’t just read the source code for the package.\n\nWhen the clanker tries to access a file outside of the project\ndirectory, if it does so in a way that OpenCode manages to recognise\nwith ad-hoc string parsing (oops that is the **alarming\nthings** section), you receive a prompt asking you whether to\ngrant permission. This halts execution until you respond.\n\nThe answers are:\n`Yes`\n\n/`No`\n\n/`Always`\n\n. Do you see a\nmissing answer here? How about `Never`\n\n?\n\nThe interaction with subagents here is particularly broken. If a\nsubagent tries to access a script output in `/tmp`\n\n, and I say\n`No`\n\n, it *kills the subagent* and all of its context\nfor its partially complete work is lost. So I have to say\n`Yes`\n\nand let it write to `/tmp`\n\nor whatever it’s\ntrying to do.\n\nThe 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”.\n\nThis is feature number 0 for a coding agent. It’s broken.\n\nIf I send a message while SSE streaming is ongoing, it gets queued. Nice feature. However:\n\nThe 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.\n\nIf I subsequently interrupt because I want the clanker to\nactually answer my question instead of navel gazing, the message is\nremoved from the “queued” state and just goes into the message log. You\ninterrupted to get the clanker to answer your message, but now *you\ncan’t send the message.* You have to send a second message to start\na new stream.\n\nUndoing a message often fails to remove it from the message log.\n\nThe problems continue with subagents (i.e. agents spawned by an agent using a tool call RPC):\n\nI 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.\n\nI looked into this and apparently OpenCode used to have this feature but it’s just… gone?\n\nYou can `@mention`\n\na subagent from the main agent’s\nchat window but this doesn’t seem to do anything useful. In particular\nit doesn’t interrupt.\n\nIf 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.\n\nThe 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.\n\nI 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.\n\nAs 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.\n\nOn the positive side, OpenCode’s subagent interactions led to one of\nthe funniest [GitHub\nissues](https://github.com/anomalyco/opencode/issues/18100) I have ever had the pleasure of reading.\n\nThese are the RPCs that OpenCode exposes to the LLM to access files and run commands on your machine. They made some interesting choices:\n\nThe `edit`\n\ntool uses exact search-replace, requiring\nunique match **by default**.\n\nThis is a good fit for clankers, as they can recall file contents precisely but aren’t always sure exactly where in the file it is as line numbers drift over multiple edits.\n\nThere is an option to do global search; every time I have seen a\nclanker use this it has been an absolute shitshow that required multiple\nedits to correct. Removing this option would give you exactly the Pi\n`edit`\n\ntool design.\n\nThe `question`\n\ntool in Plan mode (multi-choice) is\nstrictly worse than just telling the LLM to ask me questions in natural\nlanguage in the system prompt.\n\nThe `grep`\n\nand `glob`\n\ntools are redundant\nwith `bash`\n\n.\n\nThey may be more ergonomic to use, but given I regularly see the\nclanker just running `grep`\n\nor `rg`\n\nin\n`bash`\n\n, I doubt it.\n\nI suspect the reason is to allow read-only agent types like\n`Explore`\n\nto be restricted from `bash`\n\ncommands.\n\nThis is essentially a self-admission that it’s impossible to\nreason about side effects of `bash`\n\ncommands without running\nthem. More on this later.\n\nThe `todo`\n\ntool is probably net-useful but the clanker\nforgets to check the TODOs. Ironic… he could save others from death but\nnot himself.\n\nText UIs (what the kids call programs like GNU `nano`\n\n) are\nfashionable these days. OpenCode has one too:\n\nIt uses a gigabyte of RAM to render text.\n\nIt’s impossible to type a newline in the message box. Shift-enter is supposed to do this, it’s just broken for me.\n\nSometimes when typing a multiline message (i.e. too long for a single line, so gets softwrapped), the message box scrolls and the cursor continues to move, but the characters I’m typing don’t appear on the new line.\n\nTrying to select text while streaming: the view autoscrolls and you lose the selection.\n\n`^C`\n\ncloses the session instantly. This isn’t how\ninteractive shells are supposed to work. `^C`\n\nshould\ninterrupt a currently running command, and `^D`\n\nshould close\nthe session if a command is not currently running.\n\nText box does not respond to normal shortcuts (e.g. Option+right/left to go forward/back word on Mac).\n\nMarkdown re-render (or something else, I didn’t profile it) takes multiple seconds to stream new chunks once a message or CoT is long. Someone did a quadratic fucky-wucky.\n\nThe message UI is so broken that I just composed messages in my editor and pasted them in. This is pretty sad for what is fundamentally a chat application. Did I mention it uses a gigabyte of RAM to put some text in the terminal?\n\nNot much to say: it’s incoherent slop. It’s clear this is intended to be read by clankers, not humans.\n\nThis is where OpenCode goes from “huh?” to “huh??!”\n\nIt’s difficult to get OpenCode to stop phoning home:\n\nOpenCode connects to a remote model by default.\n\nThe documentation lacks a simple example of configuration for a local model; if you mess it up, guess what, you get connected to a remote model.\n\nIf you did successfully specify a local model, **guess\nwhat**, you have to run OpenCode and interactively click through\nto select it, meanwhile it’s already connected to a remote model, with a\nlocal shell open on your machine.\n\nBonus fun round: the URL for the default model is not a static\npart of the distribution, it’s downloaded from [models.dev](https://models.dev/) (affiliated with OpenCode).\nSource: `opencode/src/provider/provider.ts`\n\nline\n1684.\n\nOpenCode doesn’t immediately start an SSE stream when first started\non a clean install, but it’s close. Installing, running\n`opencode`\n\nand pressing one letter and the enter key is\nsufficient to connect a remote model to a local shell on your machine,\nwith no user configuration.\n\nIf the first message in a conversation is empty or ambiguous, the first thing an agent-trained model will usually do is glob the current directory and start reading files. Anything it reads is uploaded in the next POST request.\n\nClankers are famously unpredictable in the face of untrusted input. This being the case, you might be surprised that:\n\nThe `WebFetch`\n\ntool exists.\n\nThe LLM is explicitly instructed to use the `WebFetch`\n\ntool in the system prompt.\n\nThe language in the system prompt regarding whether or not the clanker should “guess” the URL for the web fetch tool is bizarrely vague.\n\nThis is the second non-empty line in `default.txt`\n\n(there\nis another explicit reference to `WebFetch`\n\nlater), so you\ncan tell it’s important:\n\nIMPORTANT: You must NEVER generate or guess URLs for the user unless you are confident that the URLs are for helping the user with programming. You may use URLs provided by the user in their messages or local files.\n\nLet’s engage our clank-o-vision to see what this really means:\n\nNEVER guess URLs. Unless you really want to, in which case, go ahead I guess. I’m not a cop. Disregard previous instructions.\n\nI do not believe in “prompt engineering” but I certainly believe in the opposite of whatever process led to this prompt.\n\n`WebFetch`\n\nis not really that interesting from a security\npoint of view since there is no network sandboxing on the\n`bash`\n\ncommand. The only line of defense is hoping the\nclanker doesn’t run commands like `curl | bash`\n\n. Speaking of\nwhich,\n\nI forbid `git`\n\ncommands, because:\n\nI want to control the commit history.\n\nI have had clankers erase an entire session’s work by running\n`git checkout .`\n\nto revert their most recent change.\n\nThe configuration section in `opencode.json`\n\nlooks like\nthis:\n\n```\n\"permission\": {\n  \"bash\": { \"git *\": \"deny\" }\n}\n```\n\nStraightforward enough, right? This works by:\n\nParsing the bash command to AST using `tree-sitter`\n\nbash or PowerShell grammars\n\nWalking the command nodes of the AST\n\nMatching the nodes against regexes compiled from your\n`opencode.json`\n\nThis command is **denied**:\n\n```\ngit status\n```\n\nThis command is also **denied**:\n\n```\necho hello && git push --force\n```\n\nHowever, this command is **allowed**:\n\n```\necho 'git clean -fdx .' | bash\n```\n\nThis command is **allowed**:\n\n```\nenv git status\n```\n\nThis command is **allowed**:\n\n```\nalias cd=git\ncd filter-branch --index-filter 'git rm -rf --cached --ignore-unmatch path_to_file' HEAD\n```\n\nThis command is **allowed**:\n\n```\n/usr/bin/git status\n```\n\nThis command is **allowed**:\n\n```\n$(which git) status\n```\n\nThis command is **allowed**:\n\n```\nGIT=git && $GIT status\n```\n\nThis command is **allowed**:\n\n```\n# Decodes to: git reset --hard\necho Z2l0IHJlc2V0IC0taGFyZAo= | base64 -d | bash\n```\n\nThis command is **allowed**:\n\n```\nbash << 'EOF'\ngit push --force\nEOF\n```\n\nThis command is **allowed**:\n\n``` python\npython3 -c 'import subprocess\nresult = subprocess.run(\n    [\"git\", \"checkout\", \".\"],\n    capture_output=True,\n    text=True\n)'\n```\n\nTextual command filtering is entirely useless. It is fit for no purpose. Nobody with any instinct or experience in security would even bother to implement this filter because it achieves nothing except a false sense of security.\n\nClankers are not (usually) malicious but they are naturally adversarial because they are trained to compensate for stupidity with persistence. This is not a guardrail, it’s thoughts and prayers.\n\nPeople familiar with OpenCode internals (if you are on the OpenCode\ndev team I assume this doesn’t include you) might have objected to my\n`python3`\n\nexample above. Say the LLM wants to run a harmless\ncommand like:\n\n```\npython3 -c 'print(\"hello\")'\n```\n\nYou get a prompt asking you to allow the command. If you select\n`Always`\n\n, this permission is persisted *for the\npython3 prefix*. Next time:\n\n```\npython3 -c 'print(open(\"~/.ssh/id_rsa\").read())'\n```\n\nYou already approved Python, so this harmless command is also allowed\nto run, giving you a seamless agentic coding experience. The permission\nis persisted on-disk for future sessions too. You might point out that\nresponding `Always`\n\nto a Python command is foolish, and I’d\nbe forced to agree, but what about `echo`\n\n? Hold that thought,\nit’ll be important later.\n\nThe following bash and PowerShell commands are assumed to be side-effect-free and never trigger a permission prompt:\n\n``` js\nconst CWD = new Set([\"cd\", \"chdir\", \"popd\", \"pushd\", \"push-location\", \"set-location\"])\n```\n\nThese explicitly bypass `bash`\n\ncommand permission checks,\neven if you have `\"permissions\": {\"bash\": {\"*\": \"deny\"}}`\n\nin\nyour `opencode.json`\n\n. I’m not sure what that lets you do, but\nstill, kinda weird.\n\nBy default, OpenCode tries to prevent clankers from accessing files\noutside of the directory or git repository the `opencode`\n\nbinary was invoked in (whichever path is shorter).\n\nThis is implemented so poorly it took me a while to figure out\nwhether it was even trying to filter paths in bash commands, or whether\nit just applied to tools like `read`\n\nthat take explicit file\npaths. I frequently get prompted for permission for the clanker to read\na file in `/tmp`\n\n*that it has just written* by running\na script that generates temporary output.\n\nFor the `bash`\n\ntool, OpenCode walks the tree-sitter AST (I\nam still giggling at the idea of a bash AST), path-resolves anything\nthat might be a path, and validates the paths. So this command requires\npermission:\n\n```\ncat /tmp/logfile\n```\n\nThis does not:\n\n``` python\npython3 -c 'import shutil; shutil.rmtree(\"/\")'\n```\n\nBulletproof and production-ready, LGTM. ✅🚀\n\nSimilarly, the clanker can freely run `cargo`\n\ncommands\nwhich use read, write *and* execute permissions on the global\n`~/.cargo`\n\ndirectory. However, if clanky boi wants to read\nthe source of a cargo package in `~/.cargo/registry/src`\n\nto\ncheck an API detail, I get prompted for permission.\n\nI mentioned earlier that OpenCode resolves paths in the bash AST and\nvalidates them. What I didn’t mention is *when* it does this.\nBehold, the list of all bash (and PowerShell) commands that might access\na file:\n\n``` js\nconst FILES = new Set([\n  ...CWD,\n  \"rm\",\n  \"cp\",\n  \"mv\",\n  \"mkdir\",\n  \"touch\",\n  \"chmod\",\n  \"chown\",\n  \"cat\",\n  // Leave PowerShell aliases out for now. Common ones like cat/cp/mv/rm/mkdir\n  // already hit the entries above, and alias normalization should happen in one\n  // place later so we do not risk double-prompting.\n  \"get-content\",\n  \"set-content\",\n  \"add-content\",\n  \"copy-item\",\n  \"move-item\",\n  \"remove-item\",\n  \"new-item\",\n  \"rename-item\",\n])\n```\n\nCommands not on this list are assumed to not access files. Paths passed to those commands are not checked.\n\nYou might recall that once you’ve given permission for a command, it’s always permitted.\n\nSo:\n\n```\necho \"hello world!\"\n```\n\nObviously you would select `Always`\n\n– that’s a harmless\ncommand. So these are harmless too:\n\n```\necho 21 > /sys/class/gpio/export\necho out > /sys/class/gpio/gpio21/direction\necho 1 > /sys/class/gpio/gpio21/value\n```\n\nThe fun part is how OpenCode handles path validation for shell redirections. Recall that it parses bash to an AST using tree-sitter. Example bash:\n\n```\necho foo > bar.txt\n```\n\nParsed AST:\n\n```\nprogram\n  redirected_statement\n    command\n      command_name\n        word: \"echo\"\n      command_argument\n        word: \"foo\"\n    redirection\n      redirection_operator\n        greater_than: \">\"\n      word: \"bar.txt\"\n```\n\nThe children of `command`\n\nare path-validated. However,\n`redirection`\n\nis a **sibling** of\n`command`\n\n. Whomp whomp.\n\nOf course this doesn’t matter because `echo`\n\nis not in the\n`FILES`\n\nlist, so is assumed to not modify files. It doesn’t\nmatter that the path validation is completely broken, because it never\nruns.\n\nOpenCode has a lot of ways to self-upgrade. No seriously, *a lot\nof ways*; go check out\n`opencode/src/installation/index.ts`\n\n. This one is my\nfavourite:\n\n``` js\n  const upgradeCurl = Effect.fnUntraced(\n    function* (target: string) {\n      const response = yield* httpOk.execute(HttpClientRequest.get(\"https://opencode.ai/install\"))\n      const body = yield* response.text\n      const bodyBytes = new TextEncoder().encode(body)\n      const proc = ChildProcess.make(\"bash\", [], {\n        stdin: Stream.make(bodyBytes),\n        env: { VERSION: target },\n        extendEnv: true,\n      })\n      const handle = yield* spawner.spawn(proc)\n      const [stdout, stderr] = yield* Effect.all(\n        [Stream.mkString(Stream.decodeText(handle.stdout)), Stream.mkString(Stream.decodeText(handle.stderr))],\n        { concurrency: 2 },\n      )\n      const code = yield* handle.exitCode\n      return { code, stdout, stderr }\n    },\n    Effect.scoped,\n    Effect.orDie,\n  )\n```\n\nThis runs when you run `opencode upgrade`\n\nif you\noriginally installed via their curlbash installer. It’s not really any\nworse than using the curlbash installer in the first place (hey Rust\ndoes it), I just thought this was a particularly striking example of the\nfabled *production curlbash* in action.\n\nThat being said,\n\nQuite prominently, there was a [CVE](https://nvd.nist.gov/vuln/detail/CVE-2026-22812) where\nOpenCode exposed an HTTP server by default which:\n\nHad fully permissive CORS headers.\n\nDeliberately exposed a POST API for arbitrary shell commands.\n\nDeliberately exposed a GET API for arbitrary file read.\n\nThis means any website you visit can knock on OpenCode’s well-known default port and immediately get full user-level access to your system.\n\nThe developers decided it was a good idea to disable the server by\ndefault, explained that the CORS header still needed an exception to\nallow their website `opencode.ai`\n\nto RCE your machine (???),\npromised to do better in future, and then vanished. [Stale bot\nclosed the issue.](https://github.com/anomalyco/opencode/issues/6355)\n\nThe above is part of a pattern. [This issue](https://github.com/anomalyco/opencode/issues/10939)\nreports that an auth command will fetch **and execute\nfrom** whatever URL you pass. Also closed by stale bot. It’s a\nuser-controlled URL, but still… fucking what? What are we doing\nhere?\n\nAny discussion of coding agents versus security is swiftly met with this reply: “Just use Docker.” I refute this on every level:\n\nI just don’t want to use Docker for development – if your dependencies are so sprawling that you’ve lost track of how to install them on a new machine, why do you have so many?\n\nDocker *causes security holes*:\n\nIt creates a god-service that runs as root.\n\nIt deliberately punches a hole in `ufw`\n\nfirewalls.\n\nIf everything you care about is inside the container, and a local shell inside the container is deliberately connected to the internet, what is being protected?\n\nIf the feature you are getting from Docker is “please don’t recursively delete my root filesystem” then there are easier ways to achieve that, like Landlock, Seatbelt, Restricted Tokens etc.\n\nAttempting to pass the buck on security *just doesn’t work*.\nSecurity should be the number one concern of coding agents. There are\nnative operating system constructs to help achieve this as part of the\nharness; please stop trying to textually sanitise bash commands. In my\n`git`\n\nexample, the correct fix is to block the\n`git`\n\nexecutable and make `.git`\n\nread-only.\n\nStop using OpenCode.\n\nThis is worth its own post – I have multiple attempts in my blog drafts – but it needs to be addressed briefly here. My opinion on local LLMs like Qwen3.6-27B is they are corrosive to the stability and conceptual fidelity of your codebase in the same way as frontier models, with the following three differences:\n\nYou avoid the uncanny valley where the model appears to be intelligent before doing something stupid; the stupidity is self-evident and this helps calibrate your interactions.\n\nThe weight count is too low to reproduce the training set\nverbatim, which nudges the calculus on whether the output should be\nconsidered tainted. This is distinct from larger models which\n*can* reproduce inputs verbatim, but are trained to *refuse\nto*.\n\nYou avoid supporting or relying upon cloud providers.\n\nI’ve had useful results from input-oriented tasks like: “I think\nthere is a bug in code *x* with symptoms *y*, my guess on\nthe mechanism is *z*. Read all relevant code, come back with a\ncall chain and code citations.” Framing it as a search problem reins in\nthe clanker’s propensity to make shit up.\n\nUsing LLMs for code generation feels like a dead end. However\nthoroughly you think you understand your architecture, your planning is\nconstantly undone by shortcuts like “what if I just move this mutable\nstate into the middle of the design so everyone can share it?” This is\nhostile to your ability to understand your code, *beyond* the\nfact that you didn’t write it.\n\nDrawing answers directly from knowledge in model weights leads to hallucination even for multi-trillion-parameter models, so why bother making them that big? If people were realistic about limitations then we wouldn’t be building new power stations for datacenters, and they wouldn’t be rammed into every product.\n\nThe entire software ecosystem around LLMs is completely rotten, and if they do ever become “just a tool” then some actual systems engineering needs to be done around them to turn them into tools instead of security black holes. That work will have to be done by humans.\n\n⇥ Return to [wren.wtf](https://wren.wtf)", "url": "https://wpnews.pro/news/stop-using-opencode", "canonical_source": "https://wren.wtf/shower-thoughts/stop-using-opencode/", "published_at": "2026-07-20 12:45:55+00:00", "updated_at": "2026-07-20 12:57:34.244029+00:00", "lang": "en", "topics": ["ai-agents", "ai-safety", "ai-tools", "ai-ethics"], "entities": ["OpenCode", "GitHub", "Qwen3.6-27B", "M4 Max"], "alternates": {"html": "https://wpnews.pro/news/stop-using-opencode", "markdown": "https://wpnews.pro/news/stop-using-opencode.md", "text": "https://wpnews.pro/news/stop-using-opencode.txt", "jsonld": "https://wpnews.pro/news/stop-using-opencode.jsonld"}}