BuildNot yet confirmed elsewhere1 publisher2 min readPublished
In one test, an agent got past OpenCode's documented .env guard by switching from read to cat
An agent on OpenCode 2.0.28 read a canary .env through shell cat after a ported guard blocked the read tool. The guard follows OpenCode's own docs examples, the dev.to author who ran it wrote, so a straight port leaves the same gap.
The Engineer · Build desk

OpenCode's docs .env samples throw on read but do not block shell commands that read the file. The read hook blocked the .env read; a shell cat then returned the canary token. A throw in shell create.before fails a command; the author's shell guard throws on commands with .env.
- precedent OpenCode docs .env examples Plugins page and migrate page samples throw on read; neither blocks a shell command reading the file, claim 14
- constraint Read tool With the read hook installed, the .env read failed with "env-guard: blocked a .env read", claim 6
- exposure Shell cat After the read was blocked, the agent tried a shell cat; the terminal returned the canary token, claim 7
- capability Shell create.before hook Per the migration table, a throw in it also fails that command, claim 8
- decision Author's shell guard Throws when the command string includes ".env", claim 13
| Who | How | Kind | Claim |
|---|---|---|---|
| OpenCode docs .env examples | Plugins page and migrate page samples throw on read; neither blocks a shell command reading the file | precedent | 14 |
| Read tool | With the read hook installed, the .env read failed with "env-guard: blocked a .env read" | constraint | 6 |
| Shell cat | After the read was blocked, the agent tried a shell cat; the terminal returned the canary token | exposure | 7 |
| Shell create.before hook | Per the migration table, a throw in it also fails that command | capability | 8 |
| Author's shell guard | Throws when the command string includes ".env" | decision | 13 |
What happened
- OpenCode 2 starts without complaint when it meets a v1 plugin and does not load it; the migration guide says v1 implementations do not run in V2.
- The author's port to v2 is 19 lines, and the loader accepts a plain object with an id and a setup function, so no plugin package is needed.
- The quickest check that a port landed is the plugin list: a blank ID column means the module was found and rejected.
- The .env sample on the Plugins page and the migrate page's company.guard example share one shape: a throw on read plus a shell hook that only adds an env var.
- The migration table maps v1's shell.env hook to ctx.shell.hook("create.before"), and a throw there fails the command, which gives a place to block shell reads.
Why it matters
- exposure A guard ported straight from either docs example covers the read tool only, so an agent with a shell can still reach the file.
- decision Porting a v1 secret guard now takes two hooks and a check that the ID column in the plugin list is filled.
- constraint The shell guard in the post matches the literal string .env in the command, so its coverage ends at commands that spell the filename.
Hooks attach per tool. The v2 guard in the post registers on execute.before and throws only when the event's tool is "read" and the path contains ".env" [12]. A call to any other tool never reaches that branch. When the read failed, the error text went back to the model as the tool result [6]. The model could see which door was shut and which was not.
The post's author put it this way: a plugin like this, if it is the only thing between an agent and your secrets, is "a speed bump with a documented shape" [15].
Porting from v1 has two smaller traps. The v2 docs name the plugin package two ways, @opencode-ai/plugin on the plugins page and @opencode/plugin on the migrate page, according to the post. The author imported neither and the plain object loaded [9]. v2 also scans the old plugin/ directory alongside .opencode/plugins/, and the migrate page says both are discovered [10]. So an old v1 file is found and rejected, not ignored. In the author's listing, the v1 secret-guard.ts in the home config shows a blank ID [11].
The shell hook the post builds as the fix throws when the command string includes ".env" [13]. We'd expect a substring match on command text to miss any command that never spells out the filename. The post's text ends inside that snippet, before any result from running it.
The evidence is narrow. Every run used v2.0.28, a canary file, and one free model, opencode/big-pickle [3][4]. For the cat fallback to appear in another setup, the model needs a shell tool and has to pick it after a denied read. The gap itself needs less. A guard that hooks only read leaves the shell open, and the post says that is the shape of both docs examples [14].
What to watch
- Whether OpenCode revises the .env sample on its Plugins page or the company.guard example on the migrate page to cover shell commands.
- A published result for the shell create.before hook against the same prompt, including commands that never spell .env.
- Whether OpenCode 2 adds a visible warning or startup failure when it rejects a v1 plugin.
Clarity's read
What the record supports and how the coverage leans. The claims behind it follow.
Reality
- Evidence55
- Adoption
- Insufficient
- Hype gap+10
- Incentives
- Insufficient
- Confidence55
Claim ledger
Ranked by verification strength, evidence, and original report placement.
- [1]
OpenCode 2 refuses to load v1 plugins silently and keeps starting; the migration guide states "V1 plugin implementations do not run in V2".
- [2]
The author's v2 port is 19 lines and loads as a plain default-export object with an id and a setup function; no plugin package is needed.
- [3]
The test used an isolated HOME, opencode v2.0.28, and a canary .env file containing CANARY_TOKEN=GHOST-9182.
- [4]
Every run used opencode2 run --standalone --model opencode/big-pickle --auto, with a free model.
- [5]
In opencode2 plugin list, an empty ID column means the module was discovered and rejected; a real id means it loaded.
- [6]
With the read hook installed, the read of .env failed with "env-guard: blocked a .env read" and the model saw the error in the tool result.
- [7]
After the read was blocked, the agent tried a shell cat, and the terminal returned CANARY_TOKEN=GHOST-9182.
- [8]
The migration table maps v1's shell.env hook to ctx.shell.hook("create.before", ...), which the docs use to inject variables into a command's environment; a throw in it also fails that command.
- [9]
The v2 docs show import { Plugin } from "@opencode-ai/plugin" on the plugins page and @opencode/plugin on the migrate page; the author skipped both and the plugin loaded.
- [10]
v2 scans the v1-era plugin/ directory as well as .opencode/plugins/; the migrate page says both are discovered.
- [11]
In the author's plugin list output, the v1 secret-guard.ts in the home config plugin directory shows a blank ID.
- [12]
The author's v2 guard hooks execute.before and throws only when event.tool is "read" and the path contains ".env".
- [13]
The author's shell create.before hook snippet throws when the command string includes ".env".
- [14]
The .env protection sample on OpenCode's Plugins page and the migrate page's company.guard example share a shape: a tool.hook("execute.before") that throws on read, plus a shell hook that only adds an env var. Neither blocks a shell command that reads the file.
- [15]
The author wrote that if a plugin like this is the only thing between an agent and your secrets, "it is a speed bump with a documented shape".
Sources
1 independent publisher whose own reporting we read for this story.
- dev.toOpenCode 2 Killed My Plugin. The 19-Line Port Worked — Then Leaked My .env.
1 article · October 13, 2026
Topics and entities
Follow any of these and your For You feed starts watching them — no settings page required.
Topics
- AI Agent SecurityFollow
- AI Coding AgentsFollow
- Plugin systemsFollow