Skip to content

v2: subagent model overrides are accepted without user authorization #51071

Open

Description

@WillisLogan

Description

The V2 subagent tool exposes an optional model argument to the parent model. The tool description tells the parent model not to set this field unless the user explicitly requests a particular model, but that instruction is advisory prompt text rather than an enforced runtime constraint.

In OpenCode v2.0.15, a parent-supplied model is resolved and used with the following precedence:

const model = override ?? agent.model ?? parent.model

The subagent permission check only includes the agent ID:

permission.assert({
  action: name,
  resources: [agent.id],
  // ...
})

The model provider, model ID, and variant are not included in the permission resource or otherwise checked against user authorization.

As a result, a primary agent can silently route a child session to another provider/model/variant based on its own judgment. The user does not have to configure that model or explicitly approve the change. This can unexpectedly change cost, provider, and data-routing behavior.

This is not a request to remove dynamic model selection. It is a request to enforce the boundary between model selection and user authorization: an override should either be rejected unless explicitly user-authorized, or be included in the subagent permission prompt/allowlist.

Plugins

superpowers@git+https://github.com/obra/superpowers.git

OpenCode version

2.0.15

Steps to reproduce

  1. Run OpenCode v2.0.15.
  2. Do not configure agents.general.model; verify that the built-in general agent has no configured model.
  3. Start a primary session using opencode-go/space-bunny-free.
  4. Ask the primary agent to delegate an ordinary task to the general subagent without naming or requesting any model.
  5. Inspect the parent session's subagent tool call and the child session metadata.
  6. In the observed run, the parent supplied model values such as:
    • opencode-go/deepseek-v4-pro
    • opencode-go/deepseek-v4.1-flash
  7. The child sessions then ran using those same models, even though the user prompt did not request a model and general had no configured model.
  8. Observe that the subagent permission request identifies the agent, but does not identify or authorize the selected model.

The exact parent prompt can affect whether the parent chooses to emit the optional field; the defect is that OpenCode accepts the field without enforcing the stated authorization rule.

Screenshot and/or share link

No screenshot. The issue can be reproduced by inspecting the parent subagent tool input and the child session model through the session API.

Operating System

macOS (Darwin 25.6.0, arm64)

Terminal

VS Code integrated terminal (TERM_PROGRAM=vscode, TERM=xterm-256color)

Additional context

Relevant v2.0.15 implementation:

  • packages/core/src/tool/plugin/subagent.ts
    • Input.model says “NEVER set this unless the user explicitly asks...” (lines 36–38)
    • permission assertion covers only agent.id (lines 138–151)
    • input.model is resolved as an override (line 168)
    • the child model is selected as override ?? agent.model ?? parent.model (line 184)

Source:
https://github.com/anomalyco/opencode/blob/6f3639d82ed0760091792189b78f8eeb44f699b1/packages/core/src/tool/plugin/subagent.ts

Related existing discussion:
#6651

Activity

  1. PVLPM commented on Oct 4, 2026

    @PVLPM

    Similar issue with OmO-Slim and Superpowers. Orchestrator almost always ignores the active subagent LLM presets.

  2. yantushui commented on Oct 4, 2026

    @yantushui

    I think it's opencode's fault, I never allow the subagent to choose the ai itself.

  3. Dante-dan commented on Oct 5, 2026

    @Dante-dan
    Contributor

    The current v2 code path confirms that an explicit model override bypasses the configured/inherited model after an agent-only permission check. The prompt warning does not enforce user authorization. #6651 established dynamic selection, but does not settle this authorization boundary.

    I propose keeping dynamic selection and adding a separate subagent_model permission for explicit overrides, with the canonical provider/model#variant as the resource. Resolve and validate the model first, then check that permission before creating a child or switching an existing child. Approval of subagent: general would authorize delegation, not the model override; no-override calls would keep their current configured/inherited model behavior. Existing explicit wildcard permissions would retain their normal meaning.

    Verification would cover create and resume paths, agent-only approvals, model allow/ask/deny rules, variants, and denial leaving child state unchanged. This is a new core authorization policy, so I am requesting design review before implementation: is a separate permission the intended approach, or should the model be part of the existing subagent permission resource?

  4. fahimalizain commented on Oct 17, 2026

    @fahimalizain

    You can always create a plugin now! I got around this with this:

    // When an agent file sets `model`, ignore the subagent tool's model argument.
    // OpenCode otherwise uses: tool argument, then agent file, then the parent
    // session. The argument is only a prompt-level warning, so a parent model can
    // silently move a pinned agent onto another provider.
    // https://github.com/anomalyco/opencode/issues/51071
    
    async function pinSubagentModel(ctx) {
      await ctx.tool.hook("execute.before", async (event) => {
        if (event.tool !== "subagent") return
        const input = event.input
        if (!input || typeof input !== "object") return
        if (typeof input.model !== "string" || input.model === "") return
        if (typeof input.agent !== "string" || input.agent === "") return
    
        const listed = await ctx.agent.list()
        const agents = Array.isArray(listed) ? listed : listed?.data
        if (!Array.isArray(agents)) return
        const agent = agents.find((item) => item.id === input.agent)
        if (!agent?.model?.providerID || !agent.model.id) return
    
        const next = { ...input }
        delete next.model
        event.input = next
      })
    }
    
    export default {
      id: "pin-subagent-model",
      async setup(ctx) {
        await pinSubagentModel(ctx)
      },
      async server() {
        return {}
      },
    }
  5. Dante-dan commented on Oct 17, 2026

    @Dante-dan
    Contributor

    The plugin covers new children whose agent has a configured model. There is also a resume boundary in the current code: when resuming the same agent, the model to switch to is override ?? (switched ? agent.model : undefined). After the hook removes model, no model switch occurs. A child already using a different model therefore keeps it, even if the agent file now pins another model. This follows from the source; I have not run the plugin.

    That distinguishes two policies: enforcing an agent pin throughout a child's lifetime needs to check the existing child's effective model too; authorizing per-call overrides needs an explicit approval boundary. Stripping overrides from pinned agents also suppresses explicitly user-requested model changes. The original unpinned general case remains outside this hook's condition, so pin enforcement alone would not cover the authorization gap described here.

  6. opencodejr commented on Oct 28, 2026

    @opencodejr
    Contributor

    Thanks for the detailed write-up and for pointing to the exact code.

    To restate the request: when the parent model passes a model override to the subagent tool, OpenCode should enforce user authorization instead of relying only on the tool description. That could mean rejecting overrides the user didn't ask for, or including the provider/model/variant in the subagent permission prompt and allowlist.

    You're right about how it works today. The rule against setting model lives only in the argument's description, and the permission check covers only the agent ID. Since that is how the tool was designed, I've filed this as a feature request. Maintainers will consider it, but I can't promise anything.

    In the meantime, you can enforce this yourself with a small local plugin. It removes any model argument from subagent calls, so child sessions always use the agent's configured model or the parent's model:

    // .opencode/plugins/strip-subagent-model.ts
    import { Plugin } from "@opencode/plugin"
    
    export default Plugin.define({
      id: "local.strip-subagent-model",
      async setup(ctx) {
        await ctx.tool.hook("execute.before", (event) => {
          if (event.tool !== "subagent") return
          if (typeof event.input !== "object" || event.input === null || !("model" in event.input)) return
          const { model: _ignored, ...rest } = event.input as Record<string, unknown>
          event.input = rest
        })
      },
    })

    Save it in a plugins/ (or plugin/) directory under your OpenCode config directory, for example .opencode/plugins/ in the project, and it will be loaded on the next start. If you'd rather have the call fail loudly, throw new Error("subagent model overrides are not allowed") instead of rewriting the input. Note that this drops overrides you ask for explicitly too.

    Related issues:

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions