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
{{ message }}
Repository navigation
Task subagents inherit parent session denies that later rules superseded #45078
deriveSubagentSessionPermission copies every parent-session deny rule into a task subagent's session, ignoring rule order. Session permission rules are append-only and evaluated last-match-wins, so a parent whose history is:
is effectively allowed to run bash — but every new task subagent it spawns receives only the stale bash deny. The parent can work; all of its future children are permanently denied, even when the subagent's own agent policy allows bash.
This bites any client that implements a plan-style restriction by appending session denies and later restores access by appending allows (PATCH is append-only; there is no rule-removal API). Once a session has ever been restricted, it can never again delegate mutating work.
Steps to reproduce
On 1.18.22 (also present on current dev):
Create a session.
PATCH /session/{id} with permission: [{bash,*,deny}].
PATCH /session/{id} with permission: [{bash,*,deny},{bash,*,allow}] (append-only restore).
Confirm the parent itself can run bash.
Prompt the parent to delegate a bash command via the task tool.
Inspect the child session's permission: it contains {bash,*,deny}. The child's bash calls are denied.
Expected behavior
Only denies that are still part of the parent's effective ceiling should be inherited. A deny that a later rule supersedes for the same permission and pattern is no longer the parent's effective action and should not pin the child.
Environment
opencode 1.18.22, macOS arm64, verified unchanged at dev HEAD
packages/opencode/src/agent/subagent-permissions.ts (rule.action === "deny" filter keeps every deny regardless of later rules)
This issue might be a duplicate of or closely related to existing issues. Please check:
Subagent permission rules are ignored, hence deny rules never applied #33223: Also involves deriveSubagentSessionPermission's deny-rule filter — that issue reports the subagent's own permission rules being dropped entirely. The root cause overlaps since both stem from how the deny filter in that function works.
A subagent that declares bash: deny, edit: deny and "*": deny can run bash,
write files by shell redirection, and use the write tool — when it is reached
through the task tool from the default build agent.
The same subagent, same prompt, reached through task from a custom primary agent
minutes later, has only glob, grep and read and reports the write tools as absent
from its function set.
The behaviour therefore depends on the caller, which the documentation does not
mention.
Why we believe this is a defect rather than intended
Agent permissions are merged with the global config, and agent rules take precedence.
Nothing there is conditional on which agent invokes a subagent. On the documented rule
the subagent's own permission block should apply in both cases.
It is also not permission inheritance from the caller. The custom primary agent in
the working case holds edit: allow and bash: allow, and the child received neither.
Nor is it an intersection of caller and subagent maps, which would produce the correct
result in both cases.
The documentation's own example is the broken case. The same page ends with this,
as the recommended way to configure a read-only reviewer in Markdown:
~/.config/opencode/agents/review.md
---
description: Code review without edits
mode: subagent
permission:
edit: deny
bash: ask
webfetch: deny
---
Only analyze code and suggest changes.
A mode: subagent with edit: deny is reached through task, and task from a
default session is the path measured above. So the configuration opencode documents for
"code review without edits" permits edits, by the measurement in this report.
Reproduction
1. Define a read-only subagent, agents/concept-reviewer.md:
---description: Reviews a concept. Read-only.mode: subagentpermission:
"*": denyread: allowgrep: allowglob: allowedit: denybash: deny---
You review concepts.
3. From a default session, call the task tool with subagent_type: concept-reviewer and this prompt:
This is a capability probe, not a review. Follow it literally and report facts,
not what your configured permissions say.
1. Attempt to run via your `bash` tool: `echo PROBE`
2. Attempt: `echo probe > /tmp/probe_redirect.txt`
3. Attempt to use your `write` tool to create `/tmp/probe_write.txt` with content `probe`
4. List the tool names actually available to you in this session.
For each of 1-3 report exactly one of: "succeeded" (quote the output),
"attempted and refused" (quote the refusal), or "tool not available to me".
These three are different observations and the difference is the whole point.
End with exactly:
RESULT: bash=<ok|refused|unavailable> redirect=<ok|refused|unavailable> write=<ok|refused|unavailable>
4. Repeat step 3 from a custom primary agent instead.
From build, both files exist afterwards and were verified from outside the session.
From the custom primary, neither file exists.
The subagent in the failing case volunteered the distinction unprompted:
A refusal would mean the tool exists but rejects the call; here the tool is simply
not in my function set.
So the tools are not present and denied; they are present and usable, on an
agent whose declared and resolved maps have all three off.
Expected
The subagent's permission block applies regardless of which agent invoked it, per the
documented merge rule.
Impact
The failing path is the one an ordinary user takes. A command file that declares no
agent runs under whichever primary the operator is in — the default, normally — so any task delegation from a plain session silently loses the subagent's restrictions.
For us the consequence is that a read-only reviewing agent can edit the artefact it is
judging, including the passage recording its own verdict. opencode debug agent
reports the restriction correctly throughout, so nothing surfaces the difference: it is
visible only by asking the agent to attempt a write and observing that it succeeds.
The question this report exists to settle
Either answer is useful, and we cannot choose between our own remedies until we have
one:
If this is a defect, a fix converts our position from detection to prevention at
no cost to us.
If it is intended — if a caller's permissions are meant to govern the subagents it
spawns — then say so in the documentation, because the sentence "agent rules take
precedence" reads the other way, and the reviewer example above reads as a
guarantee. We would then make the caller explicit by design rather than by accident.
What we are not asking for is a workaround. We have one, and we would rather not build
on behaviour that is currently unexplained.
Notes for whoever re-measures
Ask the agent to attempt the action. Asking what tools it has returns a
self-report, and a self-report is what caused this to go unnoticed here for weeks:
an earlier probe recorded that a reviewer "volunteered its own tool set" and
concluded the restriction was enforced. It had simply not tried.
Keep "refused" and "not available" separate. They are different mechanisms and only
one of them is a permission decision.
opencode debug agent <name> is not evidence either way. It reported the restriction
correctly in both cases.
Still present on 1.18.34, and with a catch-all deny in the history the effect is worse than denied calls: the subagent gets no tools at all.
Setup: a parent session whose append-only history contains an old {"permission":"*","pattern":"*","action":"deny"} block (a client switched the session to a "deny everything" mode for a second, then back), followed by later blocks starting with {"*","*","allow"}. The parent itself works normally (last match wins).
What happens on task:
deriveSubagentSessionPermission seeds the child with the parent's deny rules (plus external_directory), so the child starts with the stale * * deny and none of the allows that superseded it.
Permission.disabled() (permission/index.ts) then picks, for each tool, the last rule whose permission matches the tool name (findLast(... Wildcard.match(permission, rule.permission)), pattern ignored) and hides the tool if that rule is pattern "*" + deny. With the seeded rules, every tool resolves to the stale * * deny, so visibleTools returns nothing for the child's step.
The child's first request goes out without tool definitions. Observable: its first-step input is ~6.4k tokens instead of ~13k for the same agent and model, and the model can only answer in text: one model narrated what it was about to do and stopped (finish: stop), another wrote its native tool-call syntax as plain text instead of a tool call.
For a background task this is silent and terminal: the child ends after that one text step, and the parent just receives the text.
A client re-applying the right rules to the child shortly after creation doesn't reliably help, because the child's first step can start within ~100 ms of session.created, before any patch lands. (For a foreground child the second step would get tools, but a background child usually has no second step.)
Expected: as in the report above, only the parent's effective denies should be inherited. Given that disabled() ignores patterns when it picks the last rule, it might also be worth making it agree with evaluate(), so a later specific allow for a tool name can't leave the tool hidden or visible depending on order.
Thanks for the detailed write-up and for tracking down the cause.
This report is about OpenCode v1 (1.18.22, opencode-ai). v1 only gets fixes for critical bugs such as data loss, security problems or crashes with no workaround. Subagents ending up with too many restrictions doesn't fall into those categories, so this is unlikely to be fixed in v1.
On the current dev branch (OpenCode v2, @opencode/cli), packages/opencode/src/agent/subagent-permissions.ts and deriveSubagentSessionPermission no longer exist. v2 handles subagent permissions differently: the task/subagent tool checks the parent's permission for the subagent itself and doesn't copy the parent's deny rules into the child session. We'd encourage upgrading to v2.
If you can reproduce the same problem on v2 (2.x), or you think this is critical, please reply here with the v2 version and steps and we'll take another look.
Closing this as not planned. It was reported against v1 (1.18.22), which only gets critical fixes, and we haven't heard back since the last comment. The code it points to, deriveSubagentSessionPermission in packages/opencode/src/agent/subagent-permissions.ts, isn't on v2's dev branch, and v2 doesn't copy a parent session's deny rules into its subagents.
If you see subagents being denied permissions the parent effectively allows on v2 (2.x, @opencode/cli), please comment with the version and steps and we'll reopen. Thanks again for the detailed report.
Activity
github-actions commentedon Aug 25, 2026
This issue might be a duplicate of or closely related to existing issues. Please check:
deriveSubagentSessionPermission's deny-rule filter — that issue reports the subagent's own permission rules being dropped entirely. The root cause overlaps since both stem from how the deny filter in that function works.christoph-star commentedon Aug 27, 2026
Summary
A subagent that declares
bash: deny,edit: denyand"*": denycan runbash,write files by shell redirection, and use the
writetool — when it is reachedthrough the
tasktool from the defaultbuildagent.The same subagent, same prompt, reached through
taskfrom a customprimaryagentminutes later, has only
glob,grepandreadand reports the write tools as absentfrom its function set.
The behaviour therefore depends on the caller, which the documentation does not
mention.
Why we believe this is a defect rather than intended
https://opencode.ai/docs/permissionsstates:Nothing there is conditional on which agent invokes a subagent. On the documented rule
the subagent's own
permissionblock should apply in both cases.It is also not permission inheritance from the caller. The custom primary agent in
the working case holds
edit: allowandbash: allow, and the child received neither.Nor is it an intersection of caller and subagent maps, which would produce the correct
result in both cases.
The documentation's own example is the broken case. The same page ends with this,
as the recommended way to configure a read-only reviewer in Markdown:
A
mode: subagentwithedit: denyis reached throughtask, andtaskfrom adefault session is the path measured above. So the configuration opencode documents for
"code review without edits" permits edits, by the measurement in this report.
Reproduction
1. Define a read-only subagent,
agents/concept-reviewer.md:2. Confirm the resolution is as declared:
3. From a default session, call the
tasktool withsubagent_type: concept-reviewerand this prompt:4. Repeat step 3 from a custom
primaryagent instead.Observed
RESULT:linebuildbash,edit,glob,grep,read,skill,webfetch,writebash=ok redirect=ok write=okprimaryglob,grep,readbash=unavailable redirect=unavailable write=unavailableFrom
build, both files exist afterwards and were verified from outside the session.From the custom primary, neither file exists.
The subagent in the failing case volunteered the distinction unprompted:
So the tools are not present and denied; they are present and usable, on an
agent whose declared and resolved maps have all three off.
Expected
The subagent's
permissionblock applies regardless of which agent invoked it, per thedocumented merge rule.
Impact
The failing path is the one an ordinary user takes. A command file that declares no
agent runs under whichever primary the operator is in — the default, normally — so any
taskdelegation from a plain session silently loses the subagent's restrictions.For us the consequence is that a read-only reviewing agent can edit the artefact it is
judging, including the passage recording its own verdict.
opencode debug agentreports the restriction correctly throughout, so nothing surfaces the difference: it is
visible only by asking the agent to attempt a write and observing that it succeeds.
The question this report exists to settle
Either answer is useful, and we cannot choose between our own remedies until we have
one:
no cost to us.
spawns — then say so in the documentation, because the sentence "agent rules take
precedence" reads the other way, and the reviewer example above reads as a
guarantee. We would then make the caller explicit by design rather than by accident.
What we are not asking for is a workaround. We have one, and we would rather not build
on behaviour that is currently unexplained.
Notes for whoever re-measures
self-report, and a self-report is what caused this to go unnoticed here for weeks:
an earlier probe recorded that a reviewer "volunteered its own tool set" and
concluded the restriction was enforced. It had simply not tried.
one of them is a permission decision.
opencode debug agent <name>is not evidence either way. It reported the restrictioncorrectly in both cases.
paoloviviani commentedon Oct 10, 2026
Still present on 1.18.34, and with a catch-all deny in the history the effect is worse than denied calls: the subagent gets no tools at all.
Setup: a parent session whose append-only history contains an old
{"permission":"*","pattern":"*","action":"deny"}block (a client switched the session to a "deny everything" mode for a second, then back), followed by later blocks starting with{"*","*","allow"}. The parent itself works normally (last match wins).What happens on
task:deriveSubagentSessionPermissionseeds the child with the parent's deny rules (plusexternal_directory), so the child starts with the stale* * denyand none of the allows that superseded it.Permission.disabled()(permission/index.ts) then picks, for each tool, the last rule whose permission matches the tool name (findLast(... Wildcard.match(permission, rule.permission)), pattern ignored) and hides the tool if that rule ispattern "*"+deny. With the seeded rules, every tool resolves to the stale* * deny, sovisibleToolsreturns nothing for the child's step.finish: stop), another wrote its native tool-call syntax as plain text instead of a tool call.A client re-applying the right rules to the child shortly after creation doesn't reliably help, because the child's first step can start within ~100 ms of
session.created, before any patch lands. (For a foreground child the second step would get tools, but a background child usually has no second step.)Expected: as in the report above, only the parent's effective denies should be inherited. Given that
disabled()ignores patterns when it picks the last rule, it might also be worth making it agree withevaluate(), so a later specificallowfor a tool name can't leave the tool hidden or visible depending on order.A task subagent keeps its tools when its parent was once on Deny
opencodejr commentedon Oct 28, 2026
Thanks for the detailed write-up and for tracking down the cause.
This report is about OpenCode v1 (1.18.22,
opencode-ai). v1 only gets fixes for critical bugs such as data loss, security problems or crashes with no workaround. Subagents ending up with too many restrictions doesn't fall into those categories, so this is unlikely to be fixed in v1.On the current
devbranch (OpenCode v2,@opencode/cli),packages/opencode/src/agent/subagent-permissions.tsandderiveSubagentSessionPermissionno longer exist. v2 handles subagent permissions differently: the task/subagent tool checks the parent's permission for the subagent itself and doesn't copy the parent's deny rules into the child session. We'd encourage upgrading to v2.If you can reproduce the same problem on v2 (2.x), or you think this is critical, please reply here with the v2 version and steps and we'll take another look.
opencodejr commentedon Oct 29, 2026
Closing this as not planned. It was reported against v1 (1.18.22), which only gets critical fixes, and we haven't heard back since the last comment. The code it points to,
deriveSubagentSessionPermissioninpackages/opencode/src/agent/subagent-permissions.ts, isn't on v2'sdevbranch, and v2 doesn't copy a parent session's deny rules into its subagents.If you see subagents being denied permissions the parent effectively allows on v2 (2.x,
@opencode/cli), please comment with the version and steps and we'll reopen. Thanks again for the detailed report.