opencode icon indicating copy to clipboard operation
opencode copied to clipboard

`permission.ask` plugin hook is defined but not triggered

Open markerikson opened this issue 1 week ago • 1 comments

Description

Hi! First time filing an issue with OpenCode. Brand new user within the last few days. Tried using the new Permissions system from https://github.com/anomalyco/opencode/pull/6319 , tried to use it in a Plugin to customize behavior with some auto-approvals. Opus wrote the plugin for me, but it doesn't seem to be doing anything. It's being detected, loaded, and initialized, but not actually executing.

Here's the Opus-generated issue description and suggested fix:

Description

The permission.ask hook is defined in the plugin SDK types (@opencode-ai/plugin) but is never actually triggered by the permission system. This prevents plugins from intercepting permission requests and programmatically allowing/denying them.

Current Behavior

The PermissionNext.ask() function in packages/opencode/src/permission/next.ts evaluates rules and, if the result is "ask", immediately publishes a bus event for the UI:

if (rule.action === "ask") {
  const id = input.id ?? Identifier.ascending("permission")
  return new Promise<void>((resolve, reject) => {
    const info: Request = {
      id,
      ...request,
    }
    s.pending[id] = {
      info,
      resolve,
      reject,
    }
    Bus.publish(Event.Asked, info)  // Goes straight to UI
  })
}

The Plugin.trigger("permission.ask", ...) is never called.

Expected Behavior

Before prompting the user, the system should call the permission.ask plugin hook, allowing plugins to override the decision:

if (rule.action === "ask") {
  const id = input.id ?? Identifier.ascending("permission")
  const info: Request = {
    id,
    ...request,
  }
  
  // Let plugins intercept
  const hookOutput = await Plugin.trigger(
    "permission.ask",
    info,
    { status: "ask" as "ask" | "allow" | "deny" }
  )
  
  if (hookOutput.status === "allow") {
    return  // Auto-approved by plugin
  }
  if (hookOutput.status === "deny") {
    throw new DeniedError([])  // Or a new PluginDeniedError
  }
  
  // Still "ask" - prompt the user
  return new Promise<void>((resolve, reject) => {
    s.pending[id] = {
      info,
      resolve,
      reject,
    }
    Bus.publish(Event.Asked, info)
  })
}

Use Case

This would enable plugins like:

export const AutoApprovePlugin: Plugin = async (ctx) => {
  return {
    "permission.ask": async (input, output) => {
      // Auto-approve bash commands matching safe patterns
      if (input.permission === "bash") {
        const cmd = input.metadata?.command ?? input.patterns[0]
        if (isSafeCommand(cmd)) {
          output.status = "allow"
        }
      }
      
      // Auto-approve file ops in specific directories
      if (["read", "edit"].includes(input.permission)) {
        if (input.patterns.every(p => p.startsWith("/safe/path/"))) {
          output.status = "allow"
        }
      }
    },
  }
}

This is similar to Claude Code's PreToolUse hook pattern, which allows compositional command analysis (e.g., timeout 30 git status → strip wrapper → check core command).

Repro

Here's a tiny permissions plugin that demonstrates the intended usage:

// ~/.config/opencode/plugin/test-permission-hook.ts
import type { Plugin } from "@opencode-ai/plugin"
import { appendFileSync } from "fs"

const LOG = "/tmp/permission-hook-test.log"

function log(msg: string) {
  appendFileSync(LOG, `[${new Date().toISOString()}] ${msg}\n`)
}

const TestPermissionHook: Plugin = async () => {
  log("Plugin loaded")

  return {
    "permission.ask": async (input, output) => {
      log(`permission.ask called: ${input.permission}`)
      log(`  patterns: ${JSON.stringify(input.patterns)}`)
      log(`  metadata: ${JSON.stringify(input.metadata)}`)

      // Auto-approve any 'date' command as a test
      if (input.permission === "bash" && input.patterns.some(p => p.includes("date"))) {
        log(`  -> Setting status to "allow"`)
        output.status = "allow"
      }
    },
  }
}

export default TestPermissionHook

Suggested Fix

diff --git a/packages/opencode/src/permission/next.ts b/packages/opencode/src/permission/next.ts
index abc123..def456 100644
--- a/packages/opencode/src/permission/next.ts
+++ b/packages/opencode/src/permission/next.ts
@@ -5,6 +5,7 @@ import { Identifier } from "@/id/id"
 import { Instance } from "@/project/instance"
 import { Storage } from "@/storage/storage"
 import { fn } from "@/util/fn"
 import { Log } from "@/util/log"
 import { Wildcard } from "@/util/wildcard"
+import { Plugin } from "@/plugin"
 import z from "zod"
 
 export namespace PermissionNext {
@@ -97,15 +98,27 @@ export namespace PermissionNext {
       if (rule.action === "deny")
         throw new DeniedError(ruleset.filter((r) => Wildcard.match(request.permission, r.permission)))
       if (rule.action === "ask") {
         const id = input.id ?? Identifier.ascending("permission")
+        const info: Request = {
+          id,
+          ...request,
+        }
+
+        // Let plugins intercept the permission request
+        const hookResult = await Plugin.trigger(
+          "permission.ask",
+          info,
+          { status: "ask" as Action }
+        )
+
+        if (hookResult.status === "allow") {
+          log.info("plugin auto-approved", { permission: request.permission, pattern })
+          return
+        }
+        if (hookResult.status === "deny") {
+          log.info("plugin auto-denied", { permission: request.permission, pattern })
+          throw new DeniedError([{ permission: request.permission, pattern, action: "deny" }])
+        }
+
         return new Promise<void>((resolve, reject) => {
-          const info: Request = {
-            id,
-            ...request,
-          }
           s.pending[id] = {
             info,
             resolve,
             reject,
           }
           Bus.publish(Event.Asked, info)
         })
       }
       if (rule.action === "allow") continue
     }
   },
 )

Environment

  • OpenCode version: 1.1.x (post permissions PR #6319)
  • The hook type exists in @opencode-ai/plugin but implementation is missing

Plugins

CodeNomad; custom permissions auto-approve plugin

OpenCode version

1.1.2

Steps to reproduce

  1. Add the listed repro permissions plugin to OpenCode
  2. Run a command with date
  3. See that the command was not auto-approved

Screenshot and/or share link

No response

Operating System

Windows 11, WSL Ubuntu

Terminal

CodeNomad UI (server in WSL, browser UI in Windows Chrome)

markerikson avatar Jan 05 '26 23:01 markerikson