Repository navigation
permission.ask plugin hook is defined but not triggered #7006
Description
Description
Hi! First time filing an issue with OpenCode. Brand new user within the last few days. Tried using the new Permissions system from #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 TestPermissionHookSuggested 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 Permission rework #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)
Activity
github-actions commentedon Jan 5, 2026
This issue might be a duplicate of or related to existing issues. Please check:
permission.askplugin hook is defined but not triggered #7006The core issue appears to be that while the permission system was redesigned (as mentioned in PR #6319), the plugin hook integration for "permission.ask" was not fully implemented, preventing plugins from intercepting and auto-approving/denying permission requests.
Feel free to ignore if none of these address your specific case.
fix(permission): trigger permission.ask plugin hook in PermissionNext
select commentedon Jan 17, 2026
Thanks for clarifying this is a bug, the docs do not list this permission (yet) and I was hoping to use it to trigger a tts output.
feat(core): wire hooks and fix critical permission.asked registration
ndrwstn commentedon Mar 17, 2026
What's the status on this issue? I was just trying to see if I could wire up ntfy to confirm/deny permissions when I ran across this as a blocker?
markerikson commentedon Mar 17, 2026
No one seems to have done it. I have a local fork that implemented the missing pieces and it's been working fine for me. I can PR it, although I have no idea if it'll get merged.
repomaa commentedon Mar 19, 2026
There's a workaround:
ndrwstn commentedon Mar 24, 2026
@markerikson -- if you PR it, you can tag rekram1-node and ask for a review tagging this issue. The team has historically been very responsive, just super busy.
nickdavies commentedon Mar 30, 2026
@ndrwstn I put up #19453. I believe it solves this issue correctly but would appreciate a review from someone who knows the code better than I do
ndrwstn commentedon Apr 1, 2026
Sadly, I'm just a lowly user with just enough knowledge to be dangerous.
3 remaining items
yohi commentedon Jun 3, 2026
I've implemented the missing Plugin.trigger for this hook along with test coverage. I will open a PR for this.
kevholmes commentedon Jul 29, 2026
There are now several implementations addressing this issue:
I opened #39442 because the earlier implementations no longer apply cleanly to the current Effect/LayerNode architecture. It keeps static allow and deny rules authoritative, invokes plugins only when OpenCode would otherwise ask, falls back to the normal prompt if a plugin fails, and has been verified with a live allow/deny/throw round trip.
Could a maintainer confirm which direction the project wants: restoring the existing
permission.askcontract at the ask boundary, or adopting the broader Gate design from #34329? I’m happy to adjust #39442 or defer to another PR once that direction is clear.Add Cursor and OpenCode adapters (#25)
Ryan9438 commentedon Aug 20, 2026
Just passing by — and to be honest, I'm pretty new here (only been around this community for about two months), so please forgive me if I say anything wrong.
A few days ago, I wanted the agent to explain why it was accessing a file outside the project directory before asking the user for permission. While building that, I found the permission prompt in the TUI simply never fired — the
permission.askhook didn't trigger no matter what I tried. As a beginner, I actually spent quite a while debugging my own code, thinking I was doing something wrong, before I realized the problem might not be on my side.In the end I had to fall back to a system-prompt-based approach and packaged it as a tiny plugin: opencode-explained-right. It's just a simple workaround — I know the proper fix belongs here, and mine is nowhere near as good as the PRs you all have been working on.
Searching for the root cause eventually led me to this issue. I just wanted to say thanks to everyone investigating and submitting PRs — I've learned a lot just from reading this thread. No pressure to the maintainers at all, and I'd be very happy to help test once a fix lands. 🙏
fwa-wup commentedon Sep 1, 2026
Status ping + two data points from a downstream plugin maintainer who depends on this hook:
feat(permission): restore permission ask hook safely #42633 verified working on a real build (details in my review there): compiled
pull/42633/head(dd25dface) with bun 1.3.14 → 94/94 local tests (PR CI has no test job), clean typecheck, and on the resulting standalone binary the hook finally fires inopencode runmode, awaited before the prompt/auto-reject path — stock 1.18.25 with the identical probe: never fires.main(df6aecd) still has zeropermission.asktrigger call sites whilepackages/plugin/src/index.tskeeps shipping the hook type, so every plugin that registers it is silently dead. feat(permission): restore permission ask hook safely #42633 is mergeable and green; what it lacks is reviewer attention.The regression may be older than the
38e0dc9story suggests for stable releases. Static analysis of the published npm bundlesopencode-ai@1.2.26and1.2.27(both pre-38e0dc9, Mar 2026) found zero occurrences of the"permission.ask"hook literal and a dispatched-hook set containing nopermission.*hook at all — plus an empirical probe on 1.2.26: plugin loads, hook never fires. So either the call site only ever existed on dev/beta snapshot builds, or stable users lost it earlier than thought. Doesn't change the fix (same one-liner-of-a-call-site restoration + semantics in feat(permission): restore permission ask hook safely #42633), but it explains why "pin an old version" is not a workaround for anyone on stable.Please consider merging #42633 (or folding its tests into #19453's review thread) — the entire auto-approval plugin ecosystem for opencode is currently a no-op on every published build, despite the SDK advertising the hook.
lgarceau768 commentedon Sep 2, 2026
I filed #46809 as a duplicate of this before finding it — closed. Two details from it that I could not find anywhere in this thread, both small, one relevant to the PRs under review.
1. The bundled plugin docs now advertise the hook. In January the note above was "the docs do not list this permission (yet)". On 1.18.26 they do: the plugin-authoring doc shipped inside the binary lists
permission.askunder "Hook surface (mutateoutputin place; returnvoid)", betweenshell.envand theexperimental.*hooks — every one of which fires. So it is not only the SDK type advertising it now.2.
Permission.patternscarries the command, not the config pattern that matched.opencode runprints it aspermission requested: bash (git commit -m '...'). This matters for whichever restoration lands: a plugin that adds its ownpermission.bashpatterns in itsconfighook — to make a specific command askable so it can answer — cannot then tell its own permission from one the user configured for their own reasons. Answeringallowon a permission the person deliberately asked for takes away a decision they wanted.If the payload carried the matched pattern (or
always) alongside the command, a handler could scope itself to its own rules. Not a blocker for #42633, just cheaper to decide now than after.Thanks for the workaround note — the
permission.askedevent plus the reply endpoint works and is what I should have found first.NoahBPeterson commentedon Sep 6, 2026
@thdxr @rekram1-node
#47654
This is causing multiple people issues. Is there a fix incoming? :)
fwa-wup commentedon Sep 17, 2026
Status update from the same downstream plugin maintainer (two new data points, both measured today):
feat(permission): restore permission ask hook safely #42633 was closed by the automated stale-PR cleanup on 2026-09-14 (>1 month old, <2 positive reactions) — it was never reviewed or merged. The verification I posted here and in my review there (build of
dd25dface: probe-green, run-mode hook awaited before the prompt path) still stands: that fix worked. It just needs a reviewer, or a successor with one.A newer successor now exists and I could make it go green: fix(opencode): run the documented permission.ask plugin hook #47675 (opened 2026-09-06,
fix(opencode): run the documented permission.ask plugin hook, basedev, mergeable). Builtpull/47675/head(bda782e) today and ran the same isolated end-to-end harness:permission.askhook fires inrunmode, awaited before the prompt/auto-reject path;rm -rfstayed blocked,.envread stayed ask-blocked (MATRIX: ALL PASS);Details in my review on fix(opencode): run the documented permission.ask plugin hook #47675. The payload is the v2
PermissionRequest({permission, patterns[]}, notype/pattern) — worth stating in the eventual release notes since the previously declared type was the legacy shape.Current stable is still dead: stock 1.18.31 (released 2026-09-14, latest today) with the identical probe — plugin loads, hook never fires. Consistent with the earlier finding that no published stable ever dispatched this hook.
Practical ask: #47675 is small (6 files, +412/−7), tested locally at 97/97, and now has an independent build-and-run verification on record. A maintainer review would un-block every plugin that registers this hook (several issues: this one, #47654, #47674, and the closed #42633 line).
Atmostone commentedon Sep 17, 2026
There is another PR for this — #47675, against
dev. Two things from working on it that may be worth the thread's attention, both of which apply to the earlier attempts too.@athal7's bisect is right, and it can be pinned exactly. This is a regression, not an unimplemented feature. At
f015154,packages/opencode/src/permission/index.ts:127:At
38e0dc9("Move service state into InstanceState, flatten service facades", #18483, 21 Mar) that call is gone; the file's only remainingpermission.askmatch is thepermission.askedevent definition. The declaration inpackages/pluginand the docs were left pointing at a call site that had stopped existing.The declared input type is also wrong, and that bites anyone who restores only the call. The hook declares the legacy SDK
Permissionshape (type,pattern,title,time), but what the permission service hands over is the v2PermissionRequest(permission,patterns,always,tool) — three of nine fields overlap. @fwa-wup hit this on a build of #47675 and had to fix their own plugin for it. Restoring the trigger alone therefore leaves plugin authors readingundefinedfrom every field they would branch on, which is a quiet fail-open on a security path.#47675 changes the declaration to the shape that is actually passed and drops the casts. It also keeps a configured
denysettling before the hook, keeps the rules' decision when a hook returns an unrecognised status or throws, and hands the hook copies rather than the live request object.No claim that it is better than the earlier attempts — several were verified and simply never reviewed. But the provenance above applies to all of them, and may be the detail that makes this reviewable.
docs: keep one capability map, and put the vendor defects with their …
fwa-wup commentedon Sep 21, 2026
Downstream-plugin maintainer here (probe+matrix verification posted on #47675, 2026-09-17). Status update with a new fork in the road:
latest= 1.18.31):permission.askstill never fires (re-probed SILENT on 1.18.31 today-ish). Live fix candidate remains fix(opencode): run the documented permission.ask plugin hook #47675 (verified green by us: probe + full acceptance matrix on a local build; 97/97 PR tests). Mergeable, butmergeable_state=blockedon review, and the stale-PR cleanup bot will close it ~2026-10-06 unless reviewed — the same death feat(permission): restore permission ask hook safely #42633 died on 2026-09-14. We +1'd to keep the reaction floor.packages/opencode/no longer exists; the 1.xpermission.askhook and its dispatch are gone. Permissions are a new Effect-basedpermission.evaluateplugin hook (hooks.trigger("permission", "evaluate", {sessionID, agent, action, resources[], metadata, source, effect}), mutableeffect, called for real fromcore/src/permission.ts). So in v2 a permission plugin hook does fire — but with a different contract, and v2 has no GitHub Releases and no npmlatest/betaentry yet (docs: "plugin APIs may continue to change").Net: two possible resolutions now — (A) #47675 reviewed + shipped on 1.18.x, or (B) v2 reaches a release channel with
permission.evaluatestable. Until one of those happens, plugins relying on the documentedpermission.askremain inert no-ops on everything installable. Request remains the same: a maintainer pass on #47675 before the stale window closes; and if v2 is the answer, a migration note forpermission.ask→permission.evaluatein the v2 release notes.neriousy commentedon Oct 1, 2026
OpenCode v2 replaces this with the
permission.evaluateplugin hook, added in #45003. It runs each time a permission is checked and lets a plugin change the result and its message, so you can auto-approve or deny requests from a plugin.This is v2 only. The 1.x
permission.askhook still doesn't fire.Closing as completed. If something doesn't work on v2, please open a new issue.