Description
Summary
The permission.ask plugin hook in packages/opencode/src/permission/index.ts is currently wrapped in an if (!needsAsk) guard, which means it only fires for commands that already have an “allow” rule. For first-encounter commands where needsAsk=true, the hook is completely bypassed.
This effectively prevents plugins from intercepting or customizing the permission flow for first-encounter commands — the scenario where additional context from plugins is most valuable.
Current behavior
User runs new command → needsAsk=true → hook SKIPPED → standard dialog shown
User runs known command → needsAsk=false → hook fires → plugin can customize
Expected behavior
User runs any command → hook fires with current status ("ask" or "allow") → plugin can override → dialog shown accordingly
Why this matters for plugins
A plugin that wants to provide additional context about commands (e.g., risk assessment, documentation links, custom approval workflows) can only do so for commands the user has already approved. The first encounter — where additional context is most valuable — is unreachable.
Proposed fix
Remove the if (!needsAsk) guard so Plugin.trigger("permission.ask", ...) fires unconditionally. The hook already receives the current permissionStatus (“ask” or “allow”), so plugins can make informed decisions based on the existing status.
let permissionStatus: "ask" | "deny" | "allow" = needsAsk ? "ask" : "allow"
- if (!needsAsk) {
const hookResult = yield* Effect.tryPromise(() => Plugin.trigger(
"permission.ask",
{ sessionID: request.sessionID, permission: request.permission, patterns: request.patterns, metadata: request.metadata },
{ status: permissionStatus },
)).pipe(Effect.option)
if (hookResult._tag === "Some") {
permissionStatus = hookResult.value.status
}
- }
Backward compatibility
When no plugin is registered for permission.ask, behavior is identical to current
Existing plugins that only handle needsAsk=false cases continue to work (they receive status: "allow" as before)
The only difference is that plugins now also receive status: "ask" events they previously couldn’t see
Notes
This is a 2-line removal (the if and closing brace)
Backward compatible: no behavioral change when no plugin is registered for this hook
Minor test consideration: Permission.list() calls immediately after Permission.ask() may need to account for the hook firing asynchronously on the new path
I noticed the permission module has been undergoing some refactoring recently. Happy to adapt this proposal to align with the current direction if the approach changes.
Plugins
No response
OpenCode version
No response
Steps to reproduce
No response
Screenshot and/or share link
No response
Operating System
No response
Terminal
No response
Description
Summary
The permission.ask plugin hook in packages/opencode/src/permission/index.ts is currently wrapped in an if (!needsAsk) guard, which means it only fires for commands that already have an “allow” rule. For first-encounter commands where needsAsk=true, the hook is completely bypassed.
This effectively prevents plugins from intercepting or customizing the permission flow for first-encounter commands — the scenario where additional context from plugins is most valuable.
Current behavior
User runs new command → needsAsk=true → hook SKIPPED → standard dialog shown
User runs known command → needsAsk=false → hook fires → plugin can customize
Expected behavior
User runs any command → hook fires with current status ("ask" or "allow") → plugin can override → dialog shown accordingly
Why this matters for plugins
A plugin that wants to provide additional context about commands (e.g., risk assessment, documentation links, custom approval workflows) can only do so for commands the user has already approved. The first encounter — where additional context is most valuable — is unreachable.
Proposed fix
Remove the if (!needsAsk) guard so Plugin.trigger("permission.ask", ...) fires unconditionally. The hook already receives the current permissionStatus (“ask” or “allow”), so plugins can make informed decisions based on the existing status.
let permissionStatus: "ask" | "deny" | "allow" = needsAsk ? "ask" : "allow"
const hookResult = yield* Effect.tryPromise(() => Plugin.trigger(
"permission.ask",
{ sessionID: request.sessionID, permission: request.permission, patterns: request.patterns, metadata: request.metadata },
{ status: permissionStatus },
)).pipe(Effect.option)
if (hookResult._tag === "Some") {
permissionStatus = hookResult.value.status
}
Backward compatibility
When no plugin is registered for permission.ask, behavior is identical to current
Existing plugins that only handle needsAsk=false cases continue to work (they receive status: "allow" as before)
The only difference is that plugins now also receive status: "ask" events they previously couldn’t see
Notes
This is a 2-line removal (the if and closing brace)
Backward compatible: no behavioral change when no plugin is registered for this hook
Minor test consideration: Permission.list() calls immediately after Permission.ask() may need to account for the hook firing asynchronously on the new path
I noticed the permission module has been undergoing some refactoring recently. Happy to adapt this proposal to align with the current direction if the approach changes.
Plugins
No response
OpenCode version
No response
Steps to reproduce
No response
Screenshot and/or share link
No response
Operating System
No response
Terminal
No response