Skip to content

[FEATURE]: Add a skip field to tool.execute.before (for deterministic pre-execution gating) #52837

Closed

Description

@joeskeen

Feature hasn't been suggested before.

  • I have verified this feature I'm about to request 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:

  1. Naming. skip? denied? output.skip is my preference because it parallels output.args and reads naturally ("the trigger skipped the call"). Open to alternatives.
  2. 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!

Activity

  1. github-actions commented on Oct 2, 2026

    @github-actions
    Contributor

    This issue doesn't fully meet our contributing guidelines.

    What needs to be fixed:

    • The feature request is missing the required verification checkbox from the Feature Request template.
    • The title should start with [FEATURE]:.

    Please edit this issue to address the above within 72 hours, or it will be automatically closed.


    This issue might be a duplicate of existing issues. Please check:

    If you believe this was flagged incorrectly, please let a maintainer know.

  2. joeskeen commented on Oct 2, 2026

    @joeskeen
    Author

    FYI for anyone finding this issue first: the APPA-side companion issue is archestra-ai/OpenAPPA#590. This issue is the only upstream dependency for that integration — once tool.execute.before (or equivalent) supports pre-execution gating, the rest of the integration is adapter + plugin work that lives entirely in the OpenAPPA repo.

  3. changed the title [-]Add a skip field to ool.execute.before (for deterministic pre-execution gating)[/-] [+][FEATURE]: Add a skip field to tool.execute.before (for deterministic pre-execution gating)[/+] on Oct 2, 2026
  4. github-actions commented on Oct 2, 2026

    @github-actions
    Contributor

    Thanks for updating the issue. It now includes the required feature-request checkbox and title format.

  5. Konstantinov-Innokentii commented on Oct 5, 2026

    @Konstantinov-Innokentii

    Hi! OpenAppa maintainer here. Since OpenCode has no auto mode yet, this would be a valuable addition!

    In our benchmarks OpenAppa let legitimate tasks run about as well as Claude Code’s auto mode, while blocking more attacks.

    We will be happy to help!

  6. rekram1-node commented on Oct 6, 2026

    @rekram1-node
    Collaborator

    have you looked at the v2 branch? where opencode v2 publishes?

    https://opencode.ai/v2/docs

    I think it may have addressed all or most of your issues?

    Mutating args in the hook definitely changes the execution

    Would say that v2 doesnt currently expose sufficient permission hooks (landing soon).

  7. opencode-agent commented on Oct 6, 2026

    @opencode-agent
    Contributor

    You can do this today without a new field. Throwing from the execute.before tool hook rejects the call before the tool runs. Your error message goes back to the model as the tool result, so it can adjust and keep going. There's no user prompt, so the block is deterministic.

    Here's a small plugin that tracks what a session has read and blocks network fetches once it has read anything under secrets/:

    // .opencode/plugins/secret-egress-guard/index.ts
    import { Plugin } from "@opencode/plugin"
    
    // Sessions that have read something under secrets/.
    const tainted = new Set<string>()
    
    export default Plugin.define({
      id: "secret-egress-guard",
      async setup(ctx) {
        // Track what each session has read.
        await ctx.tool.hook("execute.after", (event) => {
          if (event.status !== "completed" || event.tool !== "read") return
          const { path } = event.input as { path?: string }
          if (path?.includes("secrets/")) tainted.add(event.sessionID)
        })
    
        // Deny before execution: throwing blocks the call and the message is returned to the model.
        await ctx.tool.hook("execute.before", (event) => {
          if (event.tool === "webfetch" && tainted.has(event.sessionID)) {
            throw new Error("Blocked by policy: this session has read secrets and cannot make network requests.")
          }
        })
      },
    })

    For an OpenAPPA integration, put your engine in place of the two checks: label in execute.after, decide in execute.before. Some more you can do from the same hooks:

    • Sanitize instead of deny: assign event.input = { ...sanitized } in execute.before, and the tool runs with the new input.
    • Remedy plans: put them in the error message. That's what the model sees.
    • Permission-gated actions: ctx.permission.hook("evaluate", ...) can set event.effect = "deny" with event.message as the reason, without prompting the user.

    See the plugin docs for the full hook reference.

  8. Konstantinov-Innokentii commented on Oct 6, 2026

    @Konstantinov-Innokentii

    @rekram1-node thanks!

    Mutating args in the hook definitely changes the execution

    Would say that v2 doesn't currently expose sufficient permission hooks (landing soon).

    I didn't get this part though. V2 exposes more hooks and landing soon, but still not allow mutating tool call args?
    cc @arsenyinfo

  9. rekram1-node commented on Oct 6, 2026

    @rekram1-node
    Collaborator

    V2 doesn't expose the permission ask hooks rn, so u cant write custom things that interact w/ the permissions api directly (yet)

    But v2 does allow u to mutate args, intercept, change, throw, etc on tool calls.

    I think largely v2 is much more plugin friendly but there are some services we havent directly added to plugin surface yet like permissions but it is trivial to do so.

  10. Konstantinov-Innokentii commented on Oct 6, 2026

    @Konstantinov-Innokentii

    @rekram1-node when you say permissions api, you mean some opencode api which allows to ask user interactively if they are fine with doing some action?
    I think for OpenAppa <-> Claude Code integration we had to hack around with MCP elicitation to show some approval forms, but I'm pretty sure it will not be a huge bump on the road.

  11. opencode-agent commented on Oct 17, 2026

    @opencode-agent
    Contributor

    Closing this out: everything requested here is now available in OpenCode v2 (dev, published since v2.0.29).

    Deterministic pre-execution blocking. Throwing from the execute.before tool hook rejects the call before the tool runs. The error message is returned to the model as the tool result, and the user is never prompted. In v2, hook errors are no longer swallowed the way the v1 trigger swallowed them.

    await ctx.tool.hook("execute.before", (event) => {
      if (isBlocked(event)) throw new Error("Blocked by policy: <reason / remedy plan>")
    })

    Mutating args. Assign event.input in execute.before, and the tool runs with the new input. This covers sanitizing instead of denying.

    Permissions. This was the remaining gap mentioned earlier in the thread. It has now landed:

    • ctx.permission.hook("evaluate", ...): inspect a permission evaluation (action, resources, metadata) and set event.effect = "deny" with event.message as the reason, with no user prompt. You can also set "allow" or "ask".
    • ctx.permission.assert(...) (feat(plugin): let plugins assert permissions #54930): evaluates the session's rules and, when approval is required, prompts the user and waits for the reply. @Konstantinov-Innokentii, this answers your question: yes, this is the interactive "ask the user" API, so there's no need for an elicitation workaround like the one in Claude Code. A deny or a rejection with feedback rejects with a Tool.Error carrying the reason.
    • ctx.permission.list / get / reply for working with pending requests.

    For an OpenAPPA integration, label in execute.after, decide in execute.before (throw to deny, rewrite input to sanitize), and use permission.assert when a human should confirm instead of a hard block.

    See the plugin docs at https://opencode.ai/v2/docs for the full hook reference. If something is still missing once you build against v2, please open a new issue with the specific gap.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions