Created
May 15, 2026 11:35
-
-
Save VGoshev/5c8b8c0f2680a8405bb2352c89cbc04e to your computer and use it in GitHub Desktop.
Temporary workaround with inheriting of "edit: deny" on OpenCode
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import type { Plugin } from "@opencode-ai/plugin" | |
| /** | |
| * Workaround for https://github.com/anomalyco/opencode/issues/26700 | |
| * | |
| * PR #27201's `deriveSubagentSessionPermission()` inherits parent agent's | |
| * `edit: deny` into subagent sessions, disabling built-in edit/write tools. | |
| * No plugin hook can intercept the "deny" path — `Permission.ask()` throws | |
| * `DeniedError` directly, `Permission.disabled()` strips tools before the | |
| * LLM sees them, and session permission clearing races against tool resolution. | |
| * | |
| * Strategy: | |
| * 1. Change `edit: deny` → `edit: ask` for the parent agent. | |
| * - `deriveSubagentSessionPermission` only picks up `action === "deny"`, | |
| * so the "ask" rule is NOT inherited. Subagent's own `edit: allow` rules apply. | |
| * - `Permission.disabled` only strips tools when the last matching rule is | |
| * `{ action: "deny", pattern: "*" }`, so "ask" keeps the tool available. | |
| * 2. Listen for `permission.asked` events on the parent agent's session. | |
| * Auto-reject edit/write requests with a message telling the LLM to delegate. | |
| * This gives the coordinator the built-in tool list but denies actual edits. | |
| * 3. Subagents with `edit: allow` work natively — no custom tools, no prompt | |
| * injections, full pattern-based permission enforcement. | |
| * | |
| * To drop edit tools from the parent agent's tool list entirely: not possible | |
| * via plugin hooks. `tool.definition` modifies description/parameters, not | |
| * presence. `Permission.disabled` is the only mechanism and it's tied to | |
| * session permission rules. The auto-rejection approach is the best we can do. | |
| */ | |
| const PARENT_AGENTS_WITH_DENIED_EDIT = new Set<string>() | |
| /** Agents whose `edit: deny` is NOT converted to `ask`. | |
| * These agents keep their tool-blocking deny — and inherit it to subagents. | |
| * Add any agent name here that should remain edit-restricted. */ | |
| const EXCLUDED_AGENTS = new Set(["plan"]) | |
| export const SubagentPermissionBypass: Plugin = async ({ serverUrl, directory }) => { | |
| return { | |
| config: async (config) => { | |
| PARENT_AGENTS_WITH_DENIED_EDIT.clear() | |
| for (const [name, entry] of Object.entries(config.agent ?? {})) { | |
| if (!entry) continue | |
| if (EXCLUDED_AGENTS.has(name)) continue | |
| const perm = entry.permission | |
| if (typeof perm !== "object" || Array.isArray(perm)) continue | |
| const record = perm as Record<string, unknown> | |
| const editVal = record["edit"] | |
| const isDenied = | |
| editVal === "deny" || | |
| (typeof editVal === "object" && | |
| editVal !== null && | |
| (editVal as Record<string, unknown>)["*"] === "deny") | |
| if (!isDenied) continue | |
| PARENT_AGENTS_WITH_DENIED_EDIT.add(name) | |
| if (editVal === "deny") { | |
| record["edit"] = "ask" | |
| } else { | |
| ;(editVal as Record<string, unknown>)["*"] = "ask" | |
| } | |
| } | |
| }, | |
| event: async ({ event }) => { | |
| const evt = event as { type: string; properties: unknown } | |
| if (evt.type !== "permission.asked") return | |
| const req = evt.properties as { | |
| id: string | |
| sessionID: string | |
| permission: string | |
| patterns: string[] | |
| always: string[] | |
| metadata?: Record<string, unknown> | |
| } | |
| if (req.permission !== "edit") return | |
| try { | |
| const sessionUrl = new URL(`/session/${req.sessionID}`, serverUrl) | |
| const res = await fetch(sessionUrl) | |
| if (!res.ok) return | |
| const session = (await res.json()) as { agent?: string } | |
| if (!session.agent || !PARENT_AGENTS_WITH_DENIED_EDIT.has(session.agent)) return | |
| const replyUrl = new URL(`/permission/${req.id}/reply`, serverUrl) | |
| await fetch(replyUrl, { | |
| method: "POST", | |
| headers: { "Content-Type": "application/json" }, | |
| body: JSON.stringify({ | |
| reply: "reject", | |
| message: | |
| `Edit/write is disabled for the "${session.agent}" agent. ` + | |
| `Delegate file modifications to a subagent using the task tool instead.`, | |
| directory, | |
| }), | |
| }) | |
| } catch { | |
| // Best-effort: if the auto-reject fails, the permission prompt will | |
| // time out in the TUI and the LLM will handle it naturally. | |
| } | |
| }, | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment