Feature hasn't been suggested before.
Describe the enhancement you want to request
Hi — I just watched the Fireship video on OpenAPPA ("Did a 50 year old military secret just solve agent prompt injection?") and immediately thought: this would be a great thing to be able to use with OpenCode. The blocker is one missing primitive on the plugin side, which is what this issue is about. APPA just happens to be the first concrete use case I have — the change is generally useful for any plugin that wants to deterministically deny a tool call before it runs.
Related history
This has been requested before, all closed by automation rather than on the merits:
- #20013 — same
args being discarded at call sites; closed by 60-day-inactivity bot.
- #32565 — same primitive (
skip/result on tool.execute.before); auto-closed by the contributing-guidelines bot.
- #26521 —
cancel: string variant of the same ask; auto-closed by the contributing-guidelines bot.
- #15933 — adjacent feature request (
messageID in hook payloads); closed by 60-day-inactivity bot.
I'm refiling because the underlying primitive is still missing, and the use case (deterministic pre-execution gating) is what every policy/safety plugin needs. Happy to close this as a duplicate of one of the above if a maintainer points at a preferred canonical issue.
What's missing
A plugin can hook tool.execute.before today (packages/plugin/src/index.ts:266) and mutate output.args, but the mutated args are discarded at every call site I checked — packages/opencode/src/session/tools.ts:102-132, :175-219, :255-303, :325-385, :398-490, and packages/opencode/src/session/prompt.ts:301-380 all pass the closure-captured original args to item.execute(args, ctx) and ignore the trigger's output. So a plugin cannot currently block a tool call before it executes.
What I'd like
Add a new field to the trigger output, say skip: boolean, and honor it at each call site. Something like:
// packages/plugin/src/index.ts
"tool.execute.before"?: (
input: { tool: string; sessionID: string; callID: string },
output: { args: any; skip?: boolean },
) => Promise<void>
// packages/opencode/src/session/tools.ts (illustrative)
const hookOut = { args }
yield* plugin.trigger("tool.execute.before", { tool, sessionID, callID }, hookOut)
if (hookOut.skip) {
// Synthesize a tool error the model can see.
return { title: "blocked", output: "blocked by plugin", metadata: { error: "plugin-blocked" } }
}
yield* item.execute(hookOut.args, ctx)
That's it. ~30 lines across the call sites, plus a one-line doc update.
Why not permission.ask?
I considered that, but it's interactive — the user gets a prompt and the block is not deterministic. APPA's contract is "this value cannot flow to this sink, full stop" — no prompt, no override. So I can't use it without weakening the guarantee.
Why not throw?
tool.execute.before exceptions are caught and ignored by the plugin trigger (packages/opencode/src/plugin/index.ts:285-298), so throwing is silently swallowed. Not viable.
Backwards compatibility
The new skip field is opt-in (defaults to false). No existing plugin breaks.
What I'd do
If this lands, I'll maintain an OpenCode plugin that wraps OpenAPPA in the opencode-ai org. The plugin is straightforward and follows exactly the shape Amp's appa plugin uses today — OpenAPPA already serves Claude Code, Amp, and Kagent the same way.
I'm happy to:
- Open a draft PR with the change.
- Write a small unit test that asserts
skip short-circuits the call.
- Add the field to the plugin docs in
packages/docs/.
Before I do that, two questions:
- Naming.
skip? denied? output.skip is my preference because it parallels output.args and reads naturally ("the trigger skipped the call"). Open to alternatives.
- Where the skip surfaces to the model. Two options: (a) silent — the tool returns a synthetic error block the model sees as
[blocked by plugin] <reason>, no TUI event; or (b) emit a permission.asked-style event so the TUI shows the block reason. Option (a) is what Amp plugins do ({ action: 'reject-and-continue' }); option (b) is more visible. Lean toward (a) for the first cut, but happy to do (b).
If this isn't a direction the maintainers want, I'd appreciate a pointer to a better extension point. The integration degrades to a post-hoc-only design without it (block-after-run), which is weaker but still shippable — so I'm not blocked on this landing, just constrained.
Thanks!
Feature hasn't been suggested before.
Describe the enhancement you want to request
Hi — I just watched the Fireship video on OpenAPPA ("Did a 50 year old military secret just solve agent prompt injection?") and immediately thought: this would be a great thing to be able to use with OpenCode. The blocker is one missing primitive on the plugin side, which is what this issue is about. APPA just happens to be the first concrete use case I have — the change is generally useful for any plugin that wants to deterministically deny a tool call before it runs.
Related history
This has been requested before, all closed by automation rather than on the merits:
argsbeing discarded at call sites; closed by 60-day-inactivity bot.skip/resultontool.execute.before); auto-closed by the contributing-guidelines bot.cancel: stringvariant of the same ask; auto-closed by the contributing-guidelines bot.messageIDin hook payloads); closed by 60-day-inactivity bot.I'm refiling because the underlying primitive is still missing, and the use case (deterministic pre-execution gating) is what every policy/safety plugin needs. Happy to close this as a duplicate of one of the above if a maintainer points at a preferred canonical issue.
What's missing
A plugin can hook
tool.execute.beforetoday (packages/plugin/src/index.ts:266) and mutateoutput.args, but the mutated args are discarded at every call site I checked —packages/opencode/src/session/tools.ts:102-132,:175-219,:255-303,:325-385,:398-490, andpackages/opencode/src/session/prompt.ts:301-380all pass the closure-captured originalargstoitem.execute(args, ctx)and ignore the trigger's output. So a plugin cannot currently block a tool call before it executes.What I'd like
Add a new field to the trigger output, say
skip: boolean, and honor it at each call site. Something like:That's it. ~30 lines across the call sites, plus a one-line doc update.
Why not
permission.ask?I considered that, but it's interactive — the user gets a prompt and the block is not deterministic. APPA's contract is "this value cannot flow to this sink, full stop" — no prompt, no override. So I can't use it without weakening the guarantee.
Why not throw?
tool.execute.beforeexceptions are caught and ignored by the plugin trigger (packages/opencode/src/plugin/index.ts:285-298), so throwing is silently swallowed. Not viable.Backwards compatibility
The new
skipfield is opt-in (defaults tofalse). No existing plugin breaks.What I'd do
If this lands, I'll maintain an OpenCode plugin that wraps OpenAPPA in the opencode-ai org. The plugin is straightforward and follows exactly the shape Amp's
appaplugin uses today — OpenAPPA already serves Claude Code, Amp, and Kagent the same way.I'm happy to:
skipshort-circuits the call.packages/docs/.Before I do that, two questions:
skip?denied?output.skipis my preference because it parallelsoutput.argsand reads naturally ("the trigger skipped the call"). Open to alternatives.[blocked by plugin] <reason>, no TUI event; or (b) emit apermission.asked-style event so the TUI shows the block reason. Option (a) is what Amp plugins do ({ action: 'reject-and-continue' }); option (b) is more visible. Lean toward (a) for the first cut, but happy to do (b).If this isn't a direction the maintainers want, I'd appreciate a pointer to a better extension point. The integration degrades to a post-hoc-only design without it (block-after-run), which is weaker but still shippable — so I'm not blocked on this landing, just constrained.
Thanks!