Generated from
compatibility/opencode.json,
the same record leastgrant doctor reads. Last verified 2026-09-02.
OpenCode
no adapter ships, or nothing has been established about it.
How it attaches
None. A plugin would attach through `tool.execute.before` and the permission reply endpoint; neither is sufficient. See deferredBecause.
No adapter ships for this agent. Deferred, and the reason has now been corrected twice — each time by measurement rather than by reading, which is why it is written out at length. The original note said there is no plugin-facing verdict API. There are two. Throwing from `tool.execute.before` is a synchronous, unraceable block: it held even with `permission: {bash: "allow"}` and the message reached the model verbatim. And the `event` hook receives `permission.asked` with a pending request id, which POST /permission/{requestID}/reply resolves — a plugin replying `reject` stopped a real call, and replying `once` let one run with no human anywhere. What defers it is what those two cannot cover. POST /session/{id}/shell bypasses `tool.execute.before` completely. Measured: with a plugin throwing unconditionally, the command still ran and still created its file. A permission layer with a documented endpoint that walks around it is not a permission layer. The reply channel is first-writer-wins rather than a gate. Permission.reply deletes the pending entry before resolving, so whoever answers first decides. The plugin beat an auto-approving client by 20ms; with 300ms of deliberation the auto-approver won and the call RAN. Same policy, opposite outcome, decided by latency. The endpoint is also unauthenticated by default, so the channel LeastGrant would deny through is equally open to anything on localhost to allow through. A plugin whose module throws is skipped silently, with nothing on the server output — so a broken LeastGrant would be absent rather than loud. And a second permission stack ships in the same binary. PermissionV2 has its own assert/ask/reply, sqlite-persisted grants, and twelve parallel tool implementations that call PermissionV2.assert directly; every plugin tool-hook trigger site sits outside that region. That is source-inferred, not measured — a v2 session could not be driven to complete a model turn — but it is the thing that would silently retire any adapter built on tool.execute.before. `permission.ask` remains typed and never fired, confirmed three ways, and the official docs have quietly dropped it while the types keep it. That is removal in progress, not implementation pending, so waiting for it is not indicated.
What a verdict does here
The three verdicts do not travel equally well. This is the table that decides whether LeastGrant is a prompt, a veto, or a suggestion on this agent.
| verdict | lands? | what actually happens |
|---|---|---|
| allow | partialprobe | Only through the reply channel, and only by winning the race. tool.execute.before can refuse but cannot approve. |
| ask | noneprobe | `permission.ask` is declared in the plugin types and never fired by the shipped binary — confirmed three ways, including a live plugin exporting it against a real permission request that was raised and resolved without it. The official docs have quietly stopped listing it. |
| deny | partialprobe | Two channels, both real, neither complete. Throwing from tool.execute.before is synchronous and unraceable and held against an explicit allow policy — but POST /session/{id}/shell bypasses it entirely. Replying `reject` to a pending permission works but is a race the policy can lose. |
Interactive and unattended
This agent does not tell the hook which mode it is in, so LeastGrant cannot distinguish an attended session from an unattended one and treats every session as unattended.
When the hook breaks
The question nobody asks until it matters: if LeastGrant crashes, times out, or cannot start, does the tool call still run?
- On crash — the call runs anyway. (probe) Depends where it crashes, and the case that matters fails open: a plugin that fails to load — module throws, bad export, failed npm install — is silently skipped and the agent then runs unpoliced with no visible warning.
- On timeout — not established, after unknowns by default. There is no timeout and none is configurable. Plugin.trigger awaits each hook serially with no deadline, so a hung hook hangs the agent indefinitely. Verified live.
- Can it be made to fail closed? — no.
What it can see
A verdict is only worth as much as the set of actions it is asked about. gated means LeastGrant is consulted before the thing happens; seen after means it is told afterwards and can at best withhold the result; not covered means the action happens with LeastGrant never hearing about it.
| tool class | coverage | detail |
|---|---|---|
| shell commands | partialprobe | tool.execute.before covers model-invoked tools, but POST /session/{id}/shell executes a shell command with no permission check and no event at all. Verified live against a config that denied `rm *`. |
| file reads | seen aftersource | |
| file writes | seen aftersource | |
| deletions | seen aftersource | |
| MCP calls | seen aftersource | |
| subagent spawn | unknownunknown | |
| network / web | unknownunknown |
What has actually been run
Four different things, deliberately not collapsed into one badge. Reproducing an agent's invocation is not running the agent; a passing conformance suite says our side is right and nothing about whether the host ever calls us.
- Live agent test — not done. No adapter ships, so there is nothing to run inside the real agent. Support is deferred for the reasons in deferredBecause: the one unraceable channel is bypassed by a documented HTTP endpoint, and the other is a race the policy can lose. The product itself was driven live — that is where those measurements came from — but LeastGrant was not in the loop.
- Real transport probed — not done. No adapter ships, so there is no invocation to reproduce. The shape is known and is the least of the problems here: a plugin is imported in-process from .opencode/plugin and called directly, with no subprocess, no quoting layer and no wire format between it and the host. Nothing about this deferral is about transport.
- Contract read from the shipped build — Plugin hook set, the permission reply endpoint, the tool.execute.before throw path and the shell endpoint that bypasses it established by running the real product with instrumented plugins against a deterministic model. 1.18.26 · win32 · 2026-09-02
- Conformance suite — not done. No adapter ships, so the conformance suite has nothing to drive. It would arrive with the adapter rather than after it — the suite fails a build where an adapter exists without a conformance shape, which is how Cursor was caught having never been tested.
Not exercised on darwin, linux.
What it cannot do
Not a disclaimer. The point of everything above is that this list exists and is specific.
- permission.ask is declared in the plugin types and never fired. Building on it would produce an adapter that appears installed and enforces nothing.
- A plugin that fails to load is silently skipped, with no warning, and the agent runs unpoliced.
- No hook timeout of any kind, so a slow hook hangs the agent rather than failing.
- POST /session/{id}/shell runs commands with no permission check and no event.
- Mutating tool args in a plugin changes what the permission engine evaluates, so plugin order decides the verdict.
- No adapter ships. This file records a completed evaluation, not a supported integration.
- Revisit if OpenCode wires permission.ask to the permission engine, which would make a faithful adapter straightforward.