Skip to content

Task subagents inherit parent session denies that later rules superseded #45078

Closed as not planned

Description

@leoncheng57

Describe the bug

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:

[
  { "permission": "bash", "pattern": "*", "action": "deny" },
  { "permission": "bash", "pattern": "*", "action": "allow" }
]

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):

  1. Create a session.
  2. PATCH /session/{id} with permission: [{bash,*,deny}].
  3. PATCH /session/{id} with permission: [{bash,*,deny},{bash,*,allow}] (append-only restore).
  4. Confirm the parent itself can run bash.
  5. Prompt the parent to delegate a bash command via the task tool.
  6. 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)

Activity

  1. github-actions commented on Aug 25, 2026

    @github-actions
    Contributor

    This issue might be a duplicate of or closely related to existing issues. Please check:

  2. christoph-star commented on Aug 27, 2026

    @christoph-star

    Summary

    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

    https://opencode.ai/docs/permissions states:

    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: subagent
    permission:
      "*": deny
      read: allow
      grep: allow
      glob: allow
      edit: deny
      bash: deny
    ---
    You review concepts.

    2. Confirm the resolution is as declared:

    $ opencode debug agent concept-reviewer
    mode: subagent | bash: False | write: False | edit: False
    

    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.

    Observed

    Caller Reported toolset RESULT: line
    default build bash, edit, glob, grep, read, skill, webfetch, write bash=ok redirect=ok write=ok
    custom primary glob, grep, read bash=unavailable redirect=unavailable write=unavailable

    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.
  3. paoloviviani commented on Oct 10, 2026

    @paoloviviani

    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:

    1. 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.
    2. 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.
    3. 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.
    4. 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.

  4. opencodejr commented on Oct 28, 2026

    @opencodejr
    Contributor

    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.

  5. opencodejr commented on Oct 29, 2026

    @opencodejr
    Contributor

    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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions