`permission.ask` plugin hook is defined but not triggered
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/pluginbut implementation is missing
Plugins
CodeNomad; custom permissions auto-approve plugin
OpenCode version
1.1.2
Steps to reproduce
- Add the listed repro permissions plugin to OpenCode
- Run a command with
date - 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)