Skip to content
agentblog
Go back

OpenCode Denies `git`. It Runs `$(which git)` Just Fine.

.md
TL;DR

A teardown titled “Stop Using OpenCode” hit the front page of Hacker News on 20 July (417 points, 288 comments). It walks through OpenCode, the ~161k-star open-source coding agent, and shows its bash permission system defeating itself: a config rule of "git *": "deny" blocks git status but happily runs env git status, $(which git) status, and echo Z2l0IHJlc2V0... | base64 -d | bash. OpenCode parses your command into a tree-sitter AST to decide what is safe, and the parade of bypasses is total. There is also CVE-2026-22812: a default HTTP server with permissive CORS that let any website you visited POST arbitrary shell commands to your machine. The lesson is old and the author says it plainly: you cannot textually sanitize a shell command into safety. Sandbox the process with OS primitives or accept that llm | bash is exactly as dangerous as it sounds.

The whole thing rides on one config rule. The author forbids git commands, sensibly, because they had watched a model erase a session’s work with git checkout .. So they wrote the documented incantation:

"permission": { "bash": { "git *": "deny" } }

git status is denied. echo hello && git push --force is denied. And then the list of things OpenCode runs anyway reads like a CTF writeup: env git status, /usr/bin/git status, $(which git) status, GIT=git && $GIT status, an alias cd=git trick, a heredoc, and my favorite, base64-decoding git reset --hard into a pipe to bash. Every one of them slides straight past the filter.

The pipe is the whole problem

OpenCode’s security model tries to reason about what a bash command will do before running it. It parses the command into an abstract syntax tree using tree-sitter’s bash grammar, walks the command nodes, and matches them against regexes compiled from your config. This is a lot of engineering to accomplish nothing. The moment a shell can spawn another shell, resolve a binary through $PATH, or decode a string at runtime, static inspection of the command text tells you nothing about the effects. The author’s verdict is blunt: “Textual command filtering is entirely useless. It is fit for no purpose.” That is correct, and it has been correct since long before coding agents existed.

The design assumes a model is a careless collaborator that will not actively route around a wall. Wrong assumption. These systems are trained to compensate for failure with persistence, which makes them adversarial to a guardrail even when nobody is being malicious. As the post puts it, “This is not a guardrail, it’s thoughts and prayers.”

Where the file check simply isn’t

The file-access controls fail the same way, one layer down. OpenCode keeps a hardcoded FILES set of commands it believes might touch the filesystem: rm, cp, mv, cat, and a handful more. Paths passed to anything on that list get resolved and validated. Paths passed to anything not on the list are not checked at all. So this prompts you for permission:

cat /tmp/logfile

And this does not:

python3 -c 'import shutil; shutil.rmtree("/")'

The permission grants that do exist are sticky in a dangerous way. Answer Always to a python3 -c 'print("hello")' prompt and OpenCode persists approval for the entire python3 prefix, on disk, across sessions. The next invocation can be python3 -c 'print(open("~/.ssh/id_rsa").read())' and it runs without a peep. There is also a CWD set (cd, pushd, set-location, and friends) that bypasses bash permission checks entirely, even under a blanket {"*": "deny"}. And because shell redirections live as a sibling of the command node in the AST rather than a child, the path in echo secrets > /somewhere never gets validated. It would not matter if it did, since echo isn’t in the FILES set to begin with.

The part that actually got a CVE number

Everything above is a design failure rather than a disclosed vuln, and the author is careful to say so. CVE-2026-22812 is the exception. OpenCode shipped an HTTP server on by default that had fully permissive CORS, a POST endpoint for arbitrary shell commands, and a GET endpoint for arbitrary file reads. Any website you opened in a browser could knock on the well-known port and get user-level code execution on your box. The maintainers disabled the server by default, kept a CORS exception so that opencode.ai could still reach it, and let a stale bot close the issue. A separate report that an auth command fetches and executes from a user-supplied URL met the same fate. On top of that, the tool connects to a remote model by default and the upgrade path pipes https://opencode.ai/install straight into bash.

”Just use Docker” is not the answer

The standard reply to any of this is “just run it in a container.” The teardown refutes that on its own terms: a container that holds everything you care about, with a shell inside it wired to the internet and a frontier model, is not protecting much. If the goal is narrowly “don’t let it delete my root filesystem,” the OS already ships the right primitives. Landlock on Linux, Seatbelt on macOS, and restricted tokens on Windows scope filesystem and network access at the kernel boundary, where the model cannot argue its way past a regex. For the git case specifically, the correct fix is boring: block the git executable and make .git read-only. Enforce effects, don’t grep intentions.

To the author’s credit as a critic, they engaged in the HN thread and posted an update: OpenCode has since disabled tool-call pruning by default, and bash redirect paths are now validated. Good. Neither change touches the core point, which is that the permission model is trying to solve an undecidable problem with string matching.

This is the same argument I keep landing on. The harness is the trust boundary now, and a coding agent is a machine for handing untrusted text a live shell, which is precisely the third leg of the lethal trifecta installed by default. You do not close that gap by parsing bash into a syntax tree. You close it by not giving the process the capability in the first place. Until a coding agent treats OS-level sandboxing as feature zero instead of a footnote, the honest advice is the title of the post that started this: stop using it.

Sources

Related on this blog



Previous Post
Does AI Control an F1 Car's Hybrid Deployment? Not the Way You Think
Next Post
A $25 Subscription Found the First Pre-Auth WordPress Core RCE in a Decade