Skip to content

Instantly share code, notes, and snippets.

@VGoshev
Created May 15, 2026 11:35
Show Gist options
  • Select an option

  • Save VGoshev/5c8b8c0f2680a8405bb2352c89cbc04e to your computer and use it in GitHub Desktop.

Select an option

Save VGoshev/5c8b8c0f2680a8405bb2352c89cbc04e to your computer and use it in GitHub Desktop.
Temporary workaround with inheriting of "edit: deny" on OpenCode
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