Skip to content

feat(plugin): let tool.execute.before request native permission approval #37164

Closed

Description

@shlee87

Problem

Plugins can inspect a tool call in tool.execute.before, but they cannot pause that call and request OpenCode’s native once / always / reject permission UI. They can only silently allow or throw a hard denial.

That blocks dynamic safety policies where risk is known only after plugin-side analysis. For example, a plugin can run git diff --diff-filter=D HEAD...<merge-ref> before git merge <merge-ref> and determine that the merge deletes 65 files. The desired behavior is to show a native approval prompt containing that computed reason, then continue the original tool call only when approved.

Proposed API

Expose an async permission-request capability to tool.execute.before, either as input.ask() or through an equivalent plugin context API.

"tool.execute.before": async (input, output) => {
  if (input.tool !== "bash") return
  const command = output.args.command
  const deleted = await countDeletedFiles(command)
  if (deleted <= 50) return

  await input.ask({
    permission: "bash",
    patterns: [command],
    reason: `This merge would delete ${deleted} files. Review before continuing.`,
  })
}

The request should use the existing native permission flow:

  • once: continue this tool call
  • always: persist the normal session-scoped approval for the supplied pattern
  • reject: return a permission-denied tool result and do not execute

Acceptance criteria

  • A plugin can request approval from tool.execute.before after inspecting validated tool args.
  • The request uses the existing TUI/Web permission UI rather than a plugin-specific dialog.
  • Approval resumes the original pending tool call without asking the model to issue it again.
  • Rejection prevents execution and gives the model a human-readable reason.
  • Explicit config deny rules remain terminal and cannot be bypassed by plugins.
  • Plugin request failures fail closed to the normal permission prompt, not silent allow.
  • Tests cover once, always, reject, explicit-deny precedence, and plugin failure.

Related work and distinction

Why this is general-purpose

Beyond deletion thresholds, this enables policy plugins for schema migration review, release/cutover gates, staged write access, and external governance systems. The plugin decides when a tool call needs human review; OpenCode remains the authority that renders and records the approval.

Activity

  1. TTTPOB commented on Jul 27, 2026

    @TTTPOB

    I think this issue covers the core missing capability. I would like to add a related runtime-sandbox use case that may affect where the API should live.

    Runtime use case

    A security plugin such as opencode-landstrip can sandbox each Bash tool call with Landlock, seccomp, Seatbelt, or AppContainer.

    Some permissions cannot be determined in tool.execute.before. The sandbox only discovers the actual resource after the process starts, for example when seccomp intercepts:

    openat("/home/user/.ssh/config")
    connect("127.0.0.1:5432")

    The sandbox broker must hold that operation, request approval through OpenCode, and then resume or deny the operation.

    Today, opencode-landstrip handles this through a TUI-specific dialog. That works in the official TUI, but SDK clients such as OpenChamber cannot render the prompt without Landstrip-specific integration.

    Current API status

    OpenCode already contains most of the required pieces:

    Plugin-defined tools receive an awaitable context.ask().
    The internal v2 permission service provides assert(), which evaluates policy and waits for a reply when the result is ask.
    The v2 SDK exposes session.permission.create().
    Standard permission requests already have events, pending-request listing, and reply APIs.

    However:

    tool.execute.before does not receive the same permission capability as a plugin-defined tool.
    A server plugin cannot access an awaitable permission requester outside its own custom tool body.
    session.permission.create() returns { id, effect } immediately. A plugin must implement its own event subscription, deferred map, cancellation, reconnect handling, and cleanup to wait for the reply.
    The injected PluginInput.client still uses the legacy SDK client, while the v2 client is a separate export.

    related: landstrip/landstrip#52

    I might add more thoughts here.

  2. jarkkojs commented on Jul 28, 2026

    @jarkkojs

    I think this issue covers the core missing capability. I would like to add a related runtime-sandbox use case that may affect where the API should live.

    Runtime use case

    A security plugin such as opencode-landstrip can sandbox each Bash tool call with Landlock, seccomp, Seatbelt, or AppContainer.

    Some permissions cannot be determined in tool.execute.before. The sandbox only discovers the actual resource after the process starts, for example when seccomp intercepts:

    openat("/home/user/.ssh/config") connect("127.0.0.1:5432")

    The sandbox broker must hold that operation, request approval through OpenCode, and then resume or deny the operation.

    Today, opencode-landstrip handles this through a TUI-specific dialog. That works in the official TUI, but SDK clients such as OpenChamber cannot render the prompt without Landstrip-specific integration.

    Current API status

    OpenCode already contains most of the required pieces:

    Plugin-defined tools receive an awaitable context.ask(). The internal v2 permission service provides assert(), which evaluates policy and waits for a reply when the result is ask. The v2 SDK exposes session.permission.create(). Standard permission requests already have events, pending-request listing, and reply APIs.

    However:

    tool.execute.before does not receive the same permission capability as a plugin-defined tool. A server plugin cannot access an awaitable permission requester outside its own custom tool body. session.permission.create() returns { id, effect } immediately. A plugin must implement its own event subscription, deferred map, cancellation, reconnect handling, and cleanup to wait for the reply. The injected PluginInput.client still uses the legacy SDK client, while the v2 client is a separate export.

    related: landstrip/landstrip#52

    I might add more thoughts here.

    There is a semantic difference with the permissions:

    1. Agent permissions are actually pre-launch permissions.
    2. Sandbox permissions are actually post-launch permissions.

    It is also good to keep in mind that:

    1. Agent permissions cannot be described with sandbox policy.
    2. Sandbox permissions cannot be described with agent permissions.

    Finally, consider this:

    1. Agent permissions define permissions for a primary or subagent.
    2. Sandbox permissions act on tool commands.

    Sandbox permissions need to be separate too because agent permissions we can predict but tool commands can be any possible random executable; or even malware. Malware and other misbehaving executable are the root reason why the sandbox makes sense in the first place.

    Thus, also permission queries emit from exactly two disjoint stimulis:

    1. A primary or subagent definition with "ask" permissions.
    2. Tool-command doing something that requires a permission.

    So I'm not sure what the correct actions are, just underlying important details and differences.

  3. TTTPOB commented on Jul 28, 2026

    @TTTPOB

    @rekram1-node, since you are assigned to this issue, I would appreciate your thoughts on whether this capability should be limited to tool.execute.before or exposed as a reusable plugin-level requester.

    I think this is somewhat important since opencode lacks os-level enforcement of permission settings. Exposing this capability would leave room for sandbox and policy plugins to provide that enforcement cleanly in the future.

    I may not have time (also experience) to participate deeply in the API design in the near term, but I wanted to document the runtime-sandbox requirement before the V2 plugin boundary becomes fixed. I would be happy to provide additional details about the Landstrip use case later if useful.

  4. jarkkojs commented on Jul 28, 2026

    @jarkkojs

    https://github.com/landstrip/landstrip/tree/main/packages/pi-landstrip#permission-model

    This for pi-landstrip's subagents but highlights pretty well important details and differences between agent and tool command permissions.

  5. vedantp7600-afk commented on Oct 12, 2026

    @vedantp7600-afk

    Hey! I came across your OpenCode issue about allowing security plugins to request native approval after inspecting a tool call.

    I'm building Revo Security, a runtime protection layer for AI coding agents. I'm exploring how to make risk detection and human approval work reliably.

    I'd love to hear whether you've found a good approach to this problem and what you think existing solutions are missing.

  6. rekram1-node commented on Oct 14, 2026

    @rekram1-node
    Collaborator

    As of v2.0.29, a tool.execute.before hook can call ctx.permission.assert (#54930) and wait for the answer. Approving lets the original call run, rejecting blocks it and gives the model the reason, and config deny rules stay final. For example:

    await ctx.tool.hook("execute.before", async (event) => {
      if (!(await deletesManyFiles(event))) return
      await ctx.permission.assert({
        sessionID: event.sessionID,
        agent: event.agent,
        source: { type: "tool", messageID: event.messageID, id: event.id },
        action: "large_delete",
        resources: [event.input.command],
      })
    })

    Closing this, thanks for the clear acceptance criteria!

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