You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
The model-facing subagent list ignores session permissions in V2. The description hook filters targets with only the selected agent's permissions, while tool snapshots and execution merge agent and session rules. A session-denied target stays advertised; a session-allowed target remains absent.
OpenCode version
V2 2.0.32, dev at ba06ff9e0bf2dcefbbbc3a1d6094f3572d77c84e.
Steps to reproduce
Define a visible reviewer subagent.
Give the parent agent subagent:reviewer:allow and create a session with permissions: [{ action: "subagent", resource: "reviewer", effect: "deny" }].
Inspect the subagent description sent to the model: it still lists reviewer. Calling that target is correctly denied.
Reverse the rules: an agent-level deny followed by a session-level allow or ask permits the target, but the description omits it.
The mismatch occurs in primary, compaction, and generate requests. This concerns the advertised target list; runtime permission enforcement works.
Code: packages/core/src/tool/plugin/subagent.ts, in the shared session hook. Compare Permission.configured and SessionContext.select, which merge the session rules.
Earlier report
This issue originally tracked the same omission in legacy describeTask. #48107 targeted that implementation and was closed by automated cleanup. The legacy packages/opencode package has since been removed, so the V2 fix belongs in packages/core.
The capability-summary enhancement discussed in the comments is separate from this session-permission mismatch.
This issue might be a duplicate of or closely related to existing issues. Please check:
V2 subagent tool does not enforce per-target subagent permissions #35238: V2 subagent tool does not enforce per-target subagent permissions — also reports that ToolRegistry does not reflect per-target permission rules in the model-facing tool description or at execution time, with overlapping code pointers (registry.ts, permission.ts).
If this is intentionally a focused, narrower fix scoped to describeTask only (as mentioned regarding #41100), please clarify that distinction so maintainers can track it separately from the above.
Corroboration on 2.0.18: our lanes pin the subagent tool with deny-all plus per-target allows, and the per-target filtering does work — a lane allowed only explore sees only that target. The gap we hit is the one this issue names from the other side: the tool's description carries purpose but no capability information, so after a permission denial a model has no way to discover that a child lane holds the tool it was denied — it retries or substitutes the nearest allowed tool instead of delegating. A capability summary per advertised target would fix the routing half; the catalog/description drift this issue tracks is the other half.
Thanks for the detailed report. I reproduced this on 2.0.32 and it still happens on the latest dev (d1bd20f).
With a visible reviewer subagent, the simulated model's requests showed:
Agent build allows reviewer, session denies it: the subagent tool description still lists reviewer. When the model calls it, it is correctly rejected with Permission denied: subagent.
Agent lead denies reviewer, session allows it: the description leaves reviewer out, although the merged rules allow it.
So enforcement uses both the agent and session rules, but the advertised list only uses the agent's. The script below reproduces it; it sets the session rules through the SDK and inspects the model request bodies.
Related: open PR #56979 ("fix(core): merge session permissions into subagent descriptions") looks like it targets exactly this. #48107 was the earlier legacy fix.
Reproduction script (drive.ts)
// Repro for #48106: the `subagent` tool description ignores session permissions.// Run: OPENCODE_DRIVE_MEDIA_DIR=/workspace/media opencode-drive run drive.tsimport{Effect,Stream}from"effect"import{Llm,OpenCodeDriver}from"opencode-drive"constlisted=(body: any): string=>{consttools: any[]=body?.tools??[]consttool=tools.find((t)=>(t.function?.name??t.name)==="subagent")constdesc: string=tool?.function?.description??tool?.description??""constidx=desc.indexOf("Available subagents:")if(!tool)return"(no subagent tool)"if(idx<0)return"(no 'Available subagents' list)"returndesc.slice(idx).split("\n").slice(1).map((l)=>l.split(":")[0].replace("- ","")).join(", ")}exportdefaultOpenCodeDriver.use({
...(process.env.OC_DEV ? {opencode: {dev: process.env.OC_DEV}} : {}),config: {agents: {reviewer: {mode: "subagent",description: "Reviews code"},// primary agent that allows reviewerbuild: {permissions: [{action: "subagent",resource: "reviewer",effect: "allow"}]},// primary agent that denies reviewerlead: {mode: "primary",description: "lead",permissions: [{action: "subagent",resource: "reviewer",effect: "deny"}],},},}asany,},({ ui, llm, opencode })=>Effect.gen(function*(){constbodies: any[]=[]letn=0yield*llm.serve((request)=>{// Title-generation requests carry no tools; answer them without counting.if(!Array.isArray((request.bodyasany)?.tools))returnStream.make(Llm.text("Title"))bodies.push(request.body)n++// request 2: model tries to call the denied reviewer targetif(n===2)returnStream.make(Llm.toolCall({index: 0,id: "call_rev",name: "subagent",input: {agent: "reviewer",description: "review",prompt: "review"}}),Llm.finish("tool-calls"),)constk=n<=1 ? 1 : n-1returnStream.make(Llm.text(`ok ${k}`))})// 1. baseline request (agent build allows reviewer, no session rules)yield*Effect.sleep(5000)// let the TUI finish startingyield*ui.submit("first")yield*ui.waitFor("ok 1",{timeout: 30000}).pipe(Effect.tapError(()=>Effect.flatMap(ui.capture(),(f: any)=>Effect.sync(()=>console.log("SCREEN:",JSON.stringify(f).slice(0,3000))))))constsessions: any=yield*opencode.session.list({}asany)constitems: any[]=sessions.data??sessions.items??sessionsconstsessionID=items[0].idconsole.log("baseline (build, no session rules):",listed(bodies[0]))// 2. session-level deny on revieweryield*opencode.session.update({
sessionID,permissions: [{action: "subagent",resource: "reviewer",effect: "deny"}],}asany)yield*ui.submit("second")yield*ui.waitFor("ok 2",{timeout: 30000})conststored: any=yield*opencode.session.get({ sessionID }asany)console.log("stored session permissions:",JSON.stringify((stored.data??stored).permissions??(stored.data??stored).permission))console.log("build + session deny reviewer (expected: reviewer NOT listed):",listed(bodies[1]))consttoolResult=JSON.stringify(bodies[2]?.messages??bodies[2]?.input??bodies[2]).match(/[^"]{0,40}call_rev[^]{0,300}/)?.[0]console.log("runtime result of calling reviewer:",toolResult)// 3. agent-level deny (lead) + session-level allowyield*opencode.session.switchAgent({ sessionID,agent: "lead"}asany)yield*opencode.session.update({
sessionID,permissions: [{action: "subagent",resource: "reviewer",effect: "allow"}],}asany)yield*ui.submit("third")yield*ui.waitFor("ok 3",{timeout: 30000})console.log("lead (deny) + session allow reviewer (expected: reviewer listed):",listed(bodies[3]))}),)
Run it with opencode-drive: opencode-drive run ./drive.ts. It drives opencode from your PATH; set OC_DEV to the path of an OpenCode checkout to run that from source instead.
Activity
github-actions commentedon Sep 9, 2026
This issue might be a duplicate of or closely related to existing issues. Please check:
ToolRegistrydoes not reflect per-target permission rules in the model-facing tool description or at execution time, with overlapping code pointers (registry.ts,permission.ts).deriveSubagentSessionPermission, which is the same session-vs-agent rule interaction described here.permission.taskbeing explicitly allowed — reports the complement case: a session-allowed target not appearing in the task description.If this is intentionally a focused, narrower fix scoped to
describeTaskonly (as mentioned regarding #41100), please clarify that distinction so maintainers can track it separately from the above.feelxbloom commentedon Sep 30, 2026
Corroboration on 2.0.18: our lanes pin the subagent tool with deny-all plus per-target allows, and the per-target filtering does work — a lane allowed only
exploresees only that target. The gap we hit is the one this issue names from the other side: the tool's description carries purpose but no capability information, so after a permission denial a model has no way to discover that a child lane holds the tool it was denied — it retries or substitutes the nearest allowed tool instead of delegating. A capability summary per advertised target would fix the routing half; the catalog/description drift this issue tracks is the other half.[-]Task tool description ignores session permissions[/-][+]Subagent description ignores session permissions[/+]opencodejr commentedon Oct 28, 2026
Thanks for the detailed report. I reproduced this on
2.0.32and it still happens on the latestdev(d1bd20f).With a visible
reviewersubagent, the simulated model's requests showed:buildallowsreviewer, session denies it: thesubagenttool description still listsreviewer. When the model calls it, it is correctly rejected withPermission denied: subagent.leaddeniesreviewer, session allows it: the description leavesreviewerout, although the merged rules allow it.So enforcement uses both the agent and session rules, but the advertised list only uses the agent's. The script below reproduces it; it sets the session rules through the SDK and inspects the model request bodies.
Related: open PR #56979 ("fix(core): merge session permissions into subagent descriptions") looks like it targets exactly this. #48107 was the earlier legacy fix.
Reproduction script (drive.ts)
Run it with opencode-drive:
opencode-drive run ./drive.ts. It drivesopencodefrom yourPATH; setOC_DEVto the path of an OpenCode checkout to run that from source instead.