Your coding agent runs a shell on your machine. I audited mine. A developer who maintains agentproto, an open runtime that lets AI agents run commands on a user's machine, audited the tool after a teardown of opencode revealed a CVE involving a default HTTP server with permissive CORS and arbitrary shell execution. The audit found that agentproto was vulnerable to browser drive-by attacks, cross-origin read exfiltration, and DNS rebinding, but fixed them by gating endpoints on the Origin header and validating the Host header. The developer emphasizes that textual command filtering is ineffective and that agentproto avoids it by using binary allowlisting with no shell. A coding agent that can run bash on your laptop is a remote shell on localhost. It has that threat model whether or not anyone designed for it — browser drive-bys, DNS rebinding, curl | bash , the twenty years of localhost-daemon mistakes the web already made and wrote down. Last week wren.wtf published a teardown of opencode: Stop using opencode https://wren.wtf/shower-thoughts/stop-using-opencode/ . Read it in full. The headline was a real CVE — a default HTTP server with permissive CORS and an endpoint that ran arbitrary shell commands, so any website you visited could take the machine. I maintain agentproto , an open runtime that does the same dangerous thing on purpose: a local daemon that lets agents run commands, read files, and open tunnels on the user's machine. So I read the teardown as a checklist and pointed it at my own code. Below is what I found, what I changed, and — the part that transfers — the methodology, so you can run it on whatever agent tool you use. "Textual command filtering is thoughts and prayers." — the teardown That line is the whole game. Most agent tools try to make bash safe by parsing the command string and blocking the bad ones. It doesn't work, and the article lists why: pipe to bash , base64-decode, env git , a heredoc, python3 -c , an absolute path. Every one walks past a string filter. The fix is not a better regex. It's two boundaries a string never touches: Everything that follows is one of those two. You can answer them in five minutes: 127.0.0.1 , or 0.0.0.0 ? fetch that port and read the response? Open devtools on any page and try.If you don't like your answers, you're where I was. | Threat | The mistake | The fix | |---|---|---| | Browser drive-by RCE | loopback-bypassed auth + reflected CORS on a command endpoint | Origin gate, no bypass | | Cross-origin read exfil | transcripts + a live event stream readable by any page | same gate on read routes | | DNS rebinding | Host header never checked | reject non-loopback Host on the loopback path | | Supply chain | curl \ | bash with no checksum | | Interpreter escape | allowlisting bash / node = arbitrary read | opt-in OS sandbox | | Phoning home | remote model by default | loopback bind, user-chosen model, gated tunnel | The browser drive-by was the sharpest. The endpoint that runs commands was gated by an auth check with a loopback bypass — there to let local clients through without a token. But a browser's fetch to 127.0.0.1 is loopback. With reflected-origin CORS, any page could drive it. The signal that separates a real local client from a drive-by is one header the page cannot forge: // A browser always sends Origin on a cross-origin request. // Native clients the CLI, curl, an editor send none. if origin && allowlisted origin return reject 403 A page at evil.com sends Origin: https://evil.com , which isn't allowlisted, and gets a 403 — from loopback, in the default config. Native clients send no Origin and keep working untouched. One header closed the class. Reading is exfiltration too. The same daemon served conversation transcripts and a live event stream over GET — readable cross-origin, no shell required. The Origin gate went on the read routes as well. If a route leaks state, gate it like it writes state. DNS rebinding is the follow-up: a page rebinds evil.com to 127.0.0.1 , and now Origin is only half the story. So the loopback path also validates the Host header — a rebinding request still carries its own hostname, and anything that isn't a loopback Host is refused. agentproto never parsed command strings. It spawns with shell: false and a verbatim argv , against a default-deny allowlist of binaries , not strings. That structurally kills every bypass in the teardown — there is no shell to inject into. The residual is honest: if you allowlist an interpreter — bash , node , python3 — you have allowlisted arbitrary code, and a working-directory anchor won't stop it reading ~/.ssh/id rsa . The article's own recommendation is the right one: confine at the OS, not the string. So command execution gained an opt-in sandbox — macOS Seatbelt, Linux bubblewrap — that bounds what the process can read and write, and in strict mode whether it has a network at all. This is the part I hold hardest. A sandbox you haven't watched deny something is a comment, not a control. So before writing a line of the sandbox, I made it deny something in a real process: cat ~/.ssh/id rsa returns Operation not permitted . ~/.ssh is never mounted, so the read is ENOENT ; with --unshare-net , a socket connect returns ENETUNREACH .Only then did the code get written — with the flags I had watched work — and a test that runs the real sandbox where it exists. The confinement ships with evidence, not a hope. The teardown's other thread: opencode connects to a remote model by default, pulls the default model's URL from a third party, and starts reading your files with a shell already open. The posture I hold agentproto to is the opposite, and it's boring on purpose: 127.0.0.1 , not 0.0.0.0 .Local-first is not a feature you add later. It's a default you refuse to trade away. Origin , and can't forge a loopback Host . Use that.Eight pull requests, all public. Every finding from the teardown is closed, and both sandbox backends were validated in real processes before they shipped: /mcp against the browser drive-by curl | bash installers in non-interactive contexts Host guardThe one thing the runtime already did right — argv and a binary allowlist instead of string filtering — is the reason the worst class never applied. Read the original teardown https://wren.wtf/shower-thoughts/stop-using-opencode/ . Then run the four questions against whatever agent runs commands on your machine. You'll find something.