# Your coding agent runs a shell on your machine. I audited mine.

> Source: <https://dev.to/agentiknet/your-coding-agent-runs-a-shell-on-your-machine-i-audited-mine-a1g>
> Published: 2026-07-21 23:40:08+00:00

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.
