Repository navigation
Subagent parent deny inheritance over-constrains delegated agents with explicit permissions #26700
Description
Description
Regression introduced by the fix for #26514 / #26597.
PR #26597 correctly closed a Plan Mode security bypass: a read-only parent agent could spawn a subagent that retained edit/write permissions. However, the implementation now copies all parent agent deny rules into the child subagent session. For deny-by-default multi-agent configurations, this creates a new failure mode: a deliberately restricted primary agent can no longer delegate to a deliberately more-capable subagent, because the primary agent's deny rules are appended after the subagent's allow rules and override them.
This is not about restoring the Plan Mode bypass. The issue is that the new inheritance behavior treats every parent deny as a descendant-wide permission ceiling. That is valid for Plan Mode read-only restrictions, but it breaks controller/coordinator patterns where the primary agent is intentionally narrow and delegates execution to a subagent that has its own explicit allowlist.
Our configuration style is deny-by-default:
- Start every agent with wildcard deny:
"*": { "*": "deny" } - Add explicit per-tool allow rules for that agent
- Give the primary/controller only
taskaccess to selected subagents - Give the execution subagent
read/grep/glob/list/task/bashas needed - Give the execution subagent permission to delegate to a worker sub-sub-agent
This worked in 1.14.41. In 1.14.46, the executor subagent inherits the controller's deny rules, so it loses its own tools and cannot continue the delegation flow.
Relevant code path:
- fix(task): subagent inherits parent agent's deny rules (Plan Mode security bypass) #26597 added
deriveSubagentSessionPermission() - That helper appends
parentAgent.permission.filter(rule => rule.action === "deny")into the child session permission - Runtime evaluates
Permission.merge(subagent.permission, session.permission) - Permission evaluation is last-match-wins
- Therefore inherited parent denies override the subagent's explicit allows
Observed effect:
read,grep,glob,list,task,todowrite,webfetchdisappear from the subagent tool inventorybashmay remain visible, but every command is denied at runtime because inheritedbash * -> denywins over the subagent'sbash * -> allow- The subagent reports that it only has
bash, cannot usetask, cannot read files, and cannot delegate further
Plugins
None required. This is reproducible with configuration only.
OpenCode version
Regresses in 1.14.46.
Known-good comparison: 1.14.41.
Steps to reproduce
Use a generic three-agent setup: controller -> executor -> worker. The names are arbitrary; the important part is that the primary/controller is intentionally restricted and the executor is explicitly whitelisted.
Minimal permission profile:
{
"default_agent": "controller",
"agent": {
"controller": {
"mode": "primary",
"permission": {
"*": { "*": "deny" },
"read": { "*": "deny" },
"bash": {
"*": "deny",
"git *": "allow",
"npm *": "allow"
},
"task": {
"*": "deny",
"executor": "allow"
},
"edit": { "*": "deny" },
"write": { "*": "deny" }
},
"prompt": "You are a restricted controller. Delegate implementation to executor."
},
"executor": {
"mode": "subagent",
"permission": {
"*": { "*": "deny" },
"read": { "*": "allow" },
"grep": { "*": "allow" },
"glob": { "*": "allow" },
"list": { "*": "allow" },
"bash": { "*": "allow" },
"task": {
"*": "deny",
"worker": "allow"
},
"edit": { "*": "deny" },
"write": { "*": "deny" }
},
"prompt": "You are an executor. Read files and delegate file edits to worker."
},
"worker": {
"mode": "subagent",
"permission": {
"*": { "*": "deny" },
"read": { "*": "allow" },
"edit": { "*": "allow" },
"write": { "*": "allow" },
"bash": { "*": "allow" },
"task": { "*": "deny" }
},
"prompt": "You are a terminal worker. Modify files when asked."
}
}
}Then ask controller to delegate a simple task to executor, where executor must read a file and delegate a small edit to worker.
Expected behavior:
controllercan only spawnexecutor.executorreceives its configured tools:read,grep,glob,list,bash, andtasktoworker.executorcannot edit directly, but can delegate toworker.workerreceives its own configured edit/write tools.
Actual behavior in 1.14.46:
executorinherits the controller's deny rules.- The inherited
* * -> deny,read * -> deny,bash * -> deny, andtask * -> denyrules are evaluated after executor's own allows. - Because last matching rule wins, executor's explicit allowlist is overridden.
executorcannot read, cannot usetask, and cannot run even allowlisted bash commands likegit statusornpm test.- The delegated workflow stops before the worker can be spawned.
This is the effective rule conflict for executor after #26597:
executor permission: read * -> allow
inherited controller deny: read * -> deny
last match wins: read is denied
executor permission: task worker -> allow
inherited controller deny: task * -> deny
last match wins: task is denied
executor permission: bash * -> allow
inherited controller deny: bash * -> deny
last match wins: bash is denied
A possible fix direction would be to distinguish parent restrictions that are intended to bind descendants (for example Plan Mode read-only ceilings) from parent-local restrictions that only describe what the parent agent itself may do. Another option would be an explicit config flag for permission inheritance strategy. The current behavior makes any deny-by-default primary agent over-constrain its subagents.
Screenshot and/or share link
N/A. This is a config/runtime permission inheritance issue.
Operating System
Observed on macOS, but the regression is in the permission derivation code path and should be platform-independent.
Terminal
Observed through an OpenCode agent session. Terminal is not material to the bug.
Activity
nabilfreeman commentedon May 10, 2026
Additional sanitized transcript excerpt from the real failure case. I’ve renamed our custom agent role to
executorhere to keep the example generic.Context: the primary/controller agent delegates to an
executorsubagent. Theexecutoris configured with explicit permissions forread,grep,glob,task, and limited shell/git operations, and is expected to delegate implementation work to a worker subagent. After upgrading to1.14.46, the executor reports that it has effectively lost all tools except a denied shell tool.A second fresh executor session produced the same failure:
And when explicitly instructed to delegate immediately rather than inspect files first:
This behavior disappeared after downgrading back to
1.14.41, which is why this looks tied specifically to the #26597 permission inheritance change rather than to the agent prompt or the local role definition.21pounder commentedon May 11, 2026
I was able to reproduce this locally and traced the root cause through the code.
How I reproduced it
Minimal
opencode.jsonwithdefault_agent: controller:{ "$schema": "https://opencode.ai/config.json", "default_agent": "controller", "agent": { "controller": { "mode": "primary", "permission": { "*": { "*": "deny" }, "read": { "*": "deny" }, "bash": { "*": "deny" }, "task": { "*": "deny", "executor": "allow" }, "edit": { "*": "deny" }, "write": { "*": "deny" } } }, "executor": { "mode": "subagent", "permission": { "*": { "*": "deny" }, "read": { "*": "allow" }, "bash": { "*": "allow" }, "task": { "*": "deny" }, "edit": { "*": "deny" }, "write": { "*": "deny" } } } } }When controller spawns executor via the task tool, the server log shows the executor session being created with this permission array:
The executor then reports it cannot use the Read tool or any other tool.
Root cause
deriveSubagentSessionPermission()insrc/agent/subagent-permissions.ts:24appends all parent agent deny rules into the child session's permission array:The effective ruleset is then built in
src/session/llm.ts:453as:Permission.disabled()usesfindLast, so the inheritedread * -> deny(appended at the end) wins over executor's ownread * -> allow, causing the Read tool to be removed from the tool inventory before the LLM ever sees it.What I think the fix should be
deriveSubagentSessionPermissionshould not blindly inherit all parent agent denies. The original intent of #26597 was to prevent Plan Mode bypass — i.e., stop a read-only parent from spawning a subagent that can edit files. That concern is specifically aboutedit/writepermissions.The simplest targeted fix: only inherit parent agent denies for edit-class permissions (
edit,write,apply_patch), not for every permission the parent happens to deny for its own operational reasons.Alternatively, the inherited denies could be prepended (before the subagent's own rules) rather than appended, so the subagent's explicit allows always win — though this would reopen the Plan Mode bypass for edit permissions.
I'm happy to submit a PR if the approach looks right to you.
nabilfreeman commentedon May 11, 2026
@21pounder good idea, but my 2 cents are that it feels to me like this is too opinionated a fix.
The permissions system should be general and unopinionated. Actually the PR that caused this bug is an example of forcing a specific usage pattern that has broken the wider architecture.
That's why I didn't open a PR, the maintainers need to think carefully about a general approach.
VGoshev commentedon May 11, 2026
To my opinion, inheriting denies of write actions will break some workflows as well (when we have coordinator with only task permissions and subagents for real work, including edits).
So I suggest to think about allowing plan agent to dispatch only read-only subagents (i.e. explore and scout) and each user will redefine it's permissions if needed (to add custom subagents for example).
Or at least add setting to not inherit such denies for users who need it in their workflows
vikramsg commentedon May 11, 2026
Just came across this issue myself. Feels like this was a natural consequence of relying on inheritance over composition.
Maybe the fix should be to have it as an optional flag, and make it required for the
Planagent but let users decide their own customization rather than breaking custom agent workflows.nabilfreeman commentedon May 11, 2026
@vikramsg You know, the Plan agent itself is actually the cause of a lot of headaches in this codebase. 😅
There are things like prompt injections matching the plan magic string that you cannot disable without forking Opencode!
So this could actually be one of the cleanest fixes, accept that Plan is already specially handled in a few places, make this permission thing Plan only.
Small cognitive load and the technical debt of Plan has already been accrued for a later date anyway.
vikramsg commentedon May 11, 2026
Really? Can you give any examples please. I would really like to see what is being done that does this kind of hardcoding.
nabilfreeman commentedon May 11, 2026
@vikramsg check this out:
Plan mode injects this hardcoded reminder that you can't turn off from config:
https://github.com/anomalyco/opencode/blob/dev/packages/opencode/src/session/prompt.ts#L388-L410
The actual reminder is here:
https://github.com/anomalyco/opencode/blob/dev/packages/opencode/src/session/prompt/plan.txt#L1-L9
It literally starts with:
Then switching back to build is also just another hardcoded
<system-reminder>:https://github.com/anomalyco/opencode/blob/dev/packages/opencode/src/session/prompt/build-switch.txt#L1-L5
And the newer experimental plan mode path hardcodes an even bigger inline reminder here:
https://github.com/anomalyco/opencode/blob/dev/packages/opencode/src/session/prompt.ts#L441-L510
What it means
So, you can configure a 100% identical primary agent to Plan's prompts, same permissions, but it will not behave the same as the special internal Plan mode.
Similarly, if you want to extend plan mode to maybe persist its plans into source control or something or sync them to a company Obsidian vault, it's extremely hard because you are constantly fighting this read only directive (learned this the hard way)
nabilfreeman commentedon May 11, 2026
So back on thread, basically deep root cause, Plan mode is the most opinionated part of Opencode, latent risk of trampling people building heavily customized harnesses, now delicate balance has been broken.
21pounder commentedon May 12, 2026
@nabilfreeman agreed — the edit-class filter is still opinionated. The real fix is letting agents declare which denies are descendant ceilings vs self-restrictions. A separate
inherit_permissionfield on the agent config would do this cleanly:deriveSubagentSessionPermissionpulls frominherit_permissioninstead of filteringpermission. No hardcoded lists, opt-in per agent. Happy to update the PR in that direction if maintainers agree.kitlangton commentedon May 13, 2026
This was fixed in #27201 and should be out in the next release.
The fix keeps Plan Mode’s inherited edit restriction, but stops parent agent self-restrictions like
read,bash, andtaskfrom erasing a subagent’s own explicit permissions.Thanks for the detailed repro and analysis here, it made this much easier to pin down.
vikramsg commentedon May 13, 2026
Sorry this does not in fact solve the following issue. I don't have reopening permissions, but feel like it was closed prematurely.
OrShmuel22 commentedon May 15, 2026
The deny-by-default multi-agent pattern from this issue is still partially broken after #27201. Parent
edit: denystill overrides subagentedit: allowbecause edit-class denies are unconditionally inherited. Opened #27654 which adds a subagent self-declaration check — if the subagent explicitly allows edit, the parent's deny isn't injected into its session. Works for the controller→executor→worker pattern described here.8 remaining items
fix(task): subagent explicit edit:allow overrides parent edit:deny
fix(task): subagent explicit edit:allow overrides parent edit:deny
fix(plan-mode): enforce bash restriction and scope subagent permissio…
fix(task): subagent explicit edit:allow overrides parent edit:deny