Skip to content

Conversation

@Sewer56
Copy link
Contributor

@Sewer56 Sewer56 commented Nov 26, 2025

Summary

Add hidden and permission.task options to control subagent visibility and invocation permissions.

Changes

  • Add hidden: true option to hide subagents from the @ autocomplete menu
    • 📓 This is already in dev branch
  • Add permission.task config to control which subagents an agent can invoke via the Task tool (supports allow/deny/ask with glob patterns)
  • Filter denied subagents from the Task tool description so the model doesn't attempt to invoke them
  • Bypass permission checks when users explicitly invoke subagents via @ autocomplete

Purpose

Enables orchestrator-style agent architectures where internal helper subagents should only be invoked programmatically by other agents, not directly by users, while also providing fine-grained control over which agents can call which subagents.

Modified Files

  • src/agent/agent.ts - Add hidden property and permission.task merging logic
  • src/config/config.ts - Add schema for new config options
  • src/session/prompt.ts - Track user-invoked agents, regenerate Task tool description with filtered subagents
  • src/tool/task.ts - Enforce task permissions with user override bypass
  • src/cli/cmd/tui/component/prompt/autocomplete.tsx - Filter hidden agents from autocomplete
  • test/permission-task.test.ts - Tests for subagent filtering
  • packages/sdk/js/src/v2/gen/types.gen.ts - Updated SDK types
  • packages/web/src/content/docs/agents.mdx - Documentation for new features

- Add subagents config field to agent schema for wildcard-based filtering
- Add filterSubagents helper and runtime validation in Task tool
- Add per-agent subagent filtering in prompt tool resolution
- Add comprehensive tests for subagent filtering patterns
- Document subagents config option for filtering agent invocations
- Add examples for exclusion patterns, wildcards, and precedence rules
- Include Markdown agent configuration example
- Filter @ autocomplete suggestions based on current agent's subagents configuration
- Fixes bug where all subagents were shown regardless of visibility settings
@malhashemi
Copy link
Contributor

I really hope this one gets merged, asked for this to be implemented a while back, thanks for making it happen

@Sewer56
Copy link
Contributor Author

Sewer56 commented Nov 29, 2025

@rekram1-node

@rekram1-node
Copy link
Collaborator

I think the correct approach here is to allow "task" to have granular permissions like bash does, and then you can whitelist, blacklist, wildcard match, enable/disable subagents

@malhashemi
Copy link
Contributor

I think the correct approach here is to allow "task" to have granular permissions like bash does, and then you can whitelist, blacklist, wildcard match, enable/disable subagents

@rekram1-node I actually have a question here, how does deny vs disable differ for tools/custom tools

For example with Deny would the agent still see the tool description? The main advantage of restricting subagents to primary is to reduce choice overload and reduce context. So does deny work similarly to disable in that regard?

@Sewer56
Copy link
Contributor Author

Sewer56 commented Dec 3, 2025

@rekram1-node

I believe what you're thinking of is this:

"agent": {
  "build": {
    "permission": {
      "task": {
        "subagent_name": "deny"
      }
    }
  }
}

etc.

However, I chose not to go with that approach; and there is a rather good reason.

The permission field semantically speaking declares how the LLM controlling an agent is allowed to execute a tool by default, however the task tool is a bit different than the rest of the tools, semantically speaking.

The place where it differs is that unlike others, the task tool can be invoked by humans using the @ notation, with the human being presented a list of subagents to invoke.

What this means is a human could try @ calling a subagent, and then receive an error saying this is not permitted. You can think of this as typing ! to execute a command and receiving 'you are not allowed because this command has not been whitelisted in the bash tool'. That would probably be quite weird.

In any case, the original assignment and problem statement in the 2 issues was to show/hide items. Although preventing execution of a given subagent is the desired outcome, the other outcome is to avoid discoverability of the subagent by both the LLM by hiding from the system prompt (and the user). If not removed from system prompt, an LLM could still try calling it; be denied, and then get stuck or go off the rails if there are multiple subagents with similar naming. (think coder and coder-high, etc.)

The current idiom for showing/hiding items within opencode configs is to expose a true/false at the top level; as we have with tools themselves and other parts of the config. There is no precedent for limiting user input/visibility from tool permissions, but there is for top level config items.


(Had to rewrite this, turns out my reply 10 hours ago didn't send)

@malhashemi
Copy link
Contributor

malhashemi commented Dec 3, 2025

@Sewer56
I do agree with @rekram1-node here in terms of how to disable/enable a subagent for the primary one. Having said that I do agree with you in terms of subagents should not even be mentioned in the primary agent system prompt if they are disabled. I think this goes back to my comment about how does deny vs disable works in opencode. I believe this enable/disable pattern is pretty redundant to permissions. The default behaviour for deny should be to exclude it from the system prompt. This should also not interfere with the human ability to call any subagent through @ as that should be a seperate invocation pattern. I have not reviewed the parts of the codebase that needs to be touched to achieve this, but from functionality perspective I believe that what would make most sense

@rekram1-node
Copy link
Collaborator

rekram1-node commented Dec 3, 2025

it doesnt have to be that way, if the user is @ it themselves then it could bypass the permission blocks, we already do similar stuff for cwd bypasses

But im not sure what makes the most sense there tbh

@rekram1-node
Copy link
Collaborator

Also if it is set to "deny" then the agent should see the tool as not having that subagent as an option

@shuv1337
Copy link
Contributor

shuv1337 commented Dec 3, 2025

For what it's worth, the primary use case of this PR for me (which I have been using in my fork for the last week), is hiding the subagents from the user @ autocomplete list. I have a bunch of small subagents and i restrict them all to a single dedicated primary agent. That way my @ mention list isn't polluted in day to day use with built-in agents, but i can just tab to my 'lazy agent' and then my @ list has all of the subagents there.

@Sewer56
Copy link
Contributor Author

Sewer56 commented Dec 4, 2025

My only question is if you're okay with the semantic inconsistency here.

Technically the permissions field controls (or should control) what an LLM is allowed to invoke.

But in this case, we wouldn't be doing that, the LLM is given free range to call any subagent as it wishes.

When you do an @ call, you ask the LLM to invoke on your behalf, but the LLM is really the one doing the invoking. In that vein, it could re-invoke whenever it wants, or even start guessing names of 'hidden' subagents if they are predictable.

Sure you could do a 'hack', see if the previous user message mentioned a specific subagent and disallow if that was not the case, but even that has some caveats. For instance, the LLM is given free reign to call it 0-* times. Should we only give it one call? Should we give it multiple? There is ambiguity.


In any case.

For me, part of the desired functionality is also to hide it from the humans too. I have subagents that are meant to be purely used by LLMs as part of orchestrator loops.

These subagents aren't used anywhere else but in these orchestrators, and I got multiple variants that use different models for speed/cost tradeoff. Without hiding I would have something like:

orchestrator-coder
orchestrator-coder-high
orchestrator-quality-gate-gpt5
orchestrator-quality-gate-opus
orchestrator-commit
orchestrator-planner
orchestrator-searcher-fast
orchestrator-searcher
mcp-search
github

every time I type @. With no local directories on display, because everything is taken up by the subagents that aren't meant to be invoked by humans.

@rekram1-node
Copy link
Collaborator

I get ur point about no precedent for changing the @ completions, Ig to me whatd make the most sense:

task permission says what agents the agent can access

when you have that agent selected, the @ would respect it…

But ig if you wanted the user to still @ it, it woudlnt be super messy you wouldnt have to read previous user message etc etc, whenever you parse the parts sent by user you would just pass a bypass flag like we do for other part types

I dont know which ux makes more sense tho

@Sewer56
Copy link
Contributor Author

Sewer56 commented Dec 7, 2025

You can send the bypass flag, but as noted above, there is ambiguity of whether you should allow it once, or multiple times, and the LLM could really go with either.

Easy solution is just multiple times, so that flag is essentially sticky.

I don't mind doing that, but then yeah, you still would want the override to hide it to the user.
And the real question is, where do I place this override?

There might be users who will still want manual invoke. This wasn't expressed in the issues above, but it's not impossible to imagine.

Technically speaking you could use the perms on the tool to control visibility of subagents to the LLM primary agent. And then use the subagents setting introduced here to hide it from the user, or something of the like.

You could even make subagents a global thing, but then you need local and global, in case a user wants it in some contexts but not in others. That's where the real ambiguity comes in. Imaginr trying to explain all that in docs too.

I'm open to suggestions based on common consensus. I just originally figured the easiest way forward was to have a simple toggle that flips it for everyone, with no room for confusion. For people who want an escape hatch (e.g. force call a subagent), there would still be option of just swapping primary agent to one that has the subagents available.

@malhashemi
Copy link
Contributor

Disclaimer: I designed this proposal but used Claude Opus 4.5 to help format and present it clearly.


@rekram1-node / @Sewer56

After thinking through the UX, I'd like to propose an alternative design that separates the two distinct concerns here:

  1. Human visibility: whether a subagent appears in menus/autocomplete
  2. LLM invocation control: which subagents a primary agent can spawn

These are independent problems and conflating them creates the semantic confusion discussed above.


Proposed Design

1. Human Visibility: visible property on subagents

Add a visible property (default true) that controls whether a subagent appears in the agent selection menu.

JSON config:

{
  "$schema": "https://opencode.ai/config.json",
  "agent": {
    "orchestrator-coder": {
      "description": "Internal coding subagent for orchestration loops",
      "mode": "subagent",
      "visible": false
    },
    "orchestrator-planner": {
      "description": "Internal planning subagent",
      "mode": "subagent",
      "visible": false
    },
    "code-reviewer": {
      "description": "Reviews code for best practices",
      "mode": "subagent",
      "visible": true
    }
  }
}

Markdown frontmatter:

---
description: Internal coding subagent for orchestration loops
mode: subagent
visible: false
---

You are a coding subagent. Focus on implementation tasks assigned by the orchestrator.

Rules:

  • visible: false hides the subagent from the agent menu.
  • Does NOT apply to mode: primary or mode: all agents (these are always visible and in Tab rotation)
  • Does NOT affect whether LLM can invoke the subagent (that's controlled by permissions)

2. LLM Invocation Control: permission.task

Use the existing permissions pattern to control which subagents a primary agent can spawn. This is consistent with how permission.bash works.

JSON config:

{
  "$schema": "https://opencode.ai/config.json",
  "agent": {
    "build": {
      "mode": "primary",
      "permission": {
        "task": {
          "*": "deny",
          "orchestrator-*": "allow",
          "code-reviewer": "allow"
        }
      }
    },
    "plan": {
      "mode": "primary",
      "permission": {
        "task": {
          "*": "deny"
        }
      }
    },
    "orchestrator": {
      "description": "Main orchestration agent",
      "mode": "primary",
      "permission": {
        "task": {
          "*": "deny",
          "orchestrator-coder": "allow",
          "orchestrator-planner": "allow",
          "orchestrator-quality-gate": "ask"
        }
      }
    }
  }
}

Markdown frontmatter:

---
description: Main orchestration agent
mode: primary
permission:
  task:
    "*": deny
    orchestrator-coder: allow
    orchestrator-planner: allow
    orchestrator-quality-gate: ask
---

You are an orchestrator. Delegate tasks to specialized subagents.

Permission values:

  • "allow": LLM can invoke this subagent freely
  • "ask": Prompt user for approval before spawning the subagent
  • "deny": LLM cannot invoke this subagent AND it's removed from the Task tool description in the system prompt (preventing confusion/hallucination)

Wildcard support:

  • "*": "deny" — Deny all by default (place first)
  • "orchestrator-*": "allow" — Allow all subagents starting with orchestrator-
  • Rules are evaluated sequentially; specific rules override earlier wildcards

3. Interaction with tools.task

For clarity on the hierarchy:

Config Effect
tools: { task: false } on primary Primary cannot spawn ANY subagents (no Task tool)
permission.task.X: "deny" Primary cannot spawn subagent X specifically
visible: false on subagent Human cannot see in agent menu

Example - a primary that cannot spawn subagents at all:

{
  "agent": {
    "simple-build": {
      "mode": "primary",
      "tools": {
        "task": false
      }
    }
  }
}

4. TUI Behavior Changes

When a subagent is active:

  • TUI should display the subagent name (e.g., [explore] or explore) rather than the primary agent name
  • Pressing Tab returns to the primary agent
  • To select a subagent again, user goes to /agents menu or uses keybind

Agent menu:

  • Shows all primary agents
  • Shows all mode: all agents
  • Shows only subagents where visible: true (or not set, since default is true)

5. The @ Question: Two Options

There are two valid approaches for how humans invoke subagents. I recommend Option A but presenting both for discussion:

Option A (Recommended): Move subagent invocation entirely to agent menu

  • Remove subagents from @ autocomplete entirely
  • @ is used only for file/context tagging
  • Subagents are invoked via /agents menu or keybind
  • visible: false hides from the agent menu

Rationale: Subagents are designed to be used by primary agents. If a user needs frequent direct access to a subagent, they should set mode: all which makes it a primary agent that's also available as a subagent. This creates a clean separation: @ = context, agent menu = agents.

Tradeoff: More friction for users who want quick inline subagent invocation.

Option B: Keep @ for subagents but control autocomplete

  • @ still invokes subagents
  • visible: false hides from agent menu.
  • Users who know the name can still type @hidden-subagent manually

Rationale: Preserves quick inline invocation for power users.

Tradeoff: Maintains the current inconsistency where @ is overloaded for both files and agents.


6. Complete Example

Here's a complete config showing all features:

{
  "$schema": "https://opencode.ai/config.json",
  "agent": {
    "build": {
      "mode": "primary",
      "permission": {
        "task": {
          "*": "deny",
          "code-reviewer": "allow",
          "explore": "allow",
          "general": "allow"
        }
      }
    },
    "orchestrator": {
      "description": "Automated orchestration agent",
      "mode": "primary",
      "permission": {
        "task": {
          "*": "deny",
          "orchestrator-coder": "allow",
          "orchestrator-planner": "allow",
          "orchestrator-quality-gate": "ask"
        }
      }
    },
    "orchestrator-coder": {
      "description": "Internal: Handles coding tasks for orchestrator",
      "mode": "subagent",
      "visible": false
    },
    "orchestrator-planner": {
      "description": "Internal: Handles planning for orchestrator",
      "mode": "subagent",
      "visible": false
    },
    "orchestrator-quality-gate": {
      "description": "Internal: Quality verification",
      "mode": "subagent",
      "visible": false
    },
    "code-reviewer": {
      "description": "Reviews code for best practices and issues",
      "mode": "subagent",
      "visible": true,
      "tools": {
        "write": false,
        "edit": false,
        "task": false
      }
    }
  }
}

With this config:

  • Build agent can invoke code-reviewer, explore, general but NOT any orchestrator-* subagents
  • Orchestrator agent can only invoke its own orchestrator-* subagents, with orchestrator-quality-gate requiring user approval
  • Human sees only code-reviewer in the agent menu (plus built-in explore and general)
  • Human does NOT see orchestrator-coder, orchestrator-planner, or orchestrator-quality-gate
  • code-reviewer does not see the task tool.

Summary

Concern Config Location
Hide subagent from human visible: false On the subagent
Control which subagents LLM can invoke permission.task On the primary agent
Disable Task tool entirely tools: { task: false } On the primary agent
Make subagent also a primary mode: all On the agent

This design:

Happy to discuss further or adjust based on feedback.

@Sewer56
Copy link
Contributor Author

Sewer56 commented Jan 5, 2026

Hm, caught a subtle bug. When merging latest.

  task: {
    "*": "deny",
    "orchestrator-*": "allow"
  }

Not sure when it happened, but at some point, this didn't auto activate the task tool itself.

So the subagents would be injected, but the task tool itself was not added to the list of tools.
I fixed it just now.

It will remove tool if no subagents are runnable. And it'll allow running if at least 1 is runnable. Provided at least 1 rule allows any subagent to run.

Also there was subtle change to Rule, where it now has Rule.action field. That's also adjusted.


In addition, I noticed in the new permissions system, the rules are applied in declaration order. Meaning that putting "*": "deny" as last will in fact deny everything, unlike the legacy system. I adjusted the docs to make a note of this.

All good for merging again.

@rekram1-node
Copy link
Collaborator

rekram1-node commented Jan 5, 2026

this is high on my list, im just out of town for a wedding but ill try to get this in

Edit: just got back from wedding too late tho gotta sleep

@malhashemi
Copy link
Contributor

Gents with the new permission system, I think we really need this one, right now we can already deny subagents with the the below syntax, but they still clutter the task tool description for no reason:

permission:
    task:
        "*": deny
        "codebase-*": allow

However, after looking at the code, I think the implementation here could be simplified quite a bit!

In #7042 (just merged), we exposed ctx.agent to tools so they can access the agent's permissions during description generation. We used this to fix skill filtering - the skill tool now filters itself based on permissions.

The same pattern would work beautifully here. Instead of:

  1. Building the task description with all agents
  2. Exporting TASK_DESCRIPTION and filterSubagents
  3. Having prompt.ts regenerate the description after the fact

We could just do it inside task.ts itself:

export const TaskTool = Tool.define("task", async (ctx) => {
  const agents = await Agent.list().then((x) => x.filter((a) => a.mode !== "primary"))
  
  // Filter by permissions - same pattern as skill.ts
  const agent = ctx?.agent
  const accessibleAgents = agent
    ? agents.filter((a) => {
        const rule = PermissionNext.evaluate("task", a.name, agent.permission)
        return rule.action !== "deny"
      })
    : agents
  const description = DESCRIPTION.replace("{agents}", 
    accessibleAgents.map((a) => `- ${a.name}: ${a.description ?? "..."}`).join("\n")
  )
  
  return { description, parameters, execute }
})

This keeps all the filtering logic self-contained in the tool (consistent with how skill.ts works now), avoids the coupling between task.ts and prompt.ts, and is way fewer lines of code 😄

The userInvokedAgents bypass logic might still be needed for the runtime permission check - that part seems fine. But the description filtering could definitely be cleaner!

Happy to help out if you'd like - just let me know!

@Sewer56
Copy link
Contributor Author

Sewer56 commented Jan 7, 2026

I'm happy to get extra hands. Continually updating this to keep up with dev for the past 2 months has taken some time alright.

Feel free to drop a patch, or I can just give you write access to my fork, whatever works.

Just remember, that this has to account for the user switching the primary agent. That's why it was done the way it was originally at the time.

@malhashemi
Copy link
Contributor

I'm happy to get extra hands. Continually updating this to keep up with dev for the past 2 months has taken some time alright.

Feel free to drop a patch, or I can just give you write access to my fork, whatever works.

If you can grant me write access I would be happy to help

Just remember, that this has to account for the user switching the primary agent. That's why it was done the way it was originally at the time.

For sure that's the whole value proposition, skill filtration works in the same way.

@rekram1-node
Copy link
Collaborator

hey today is my first day back I said id merge it :)

@Sewer56
Copy link
Contributor Author

Sewer56 commented Jan 7, 2026

>w<

@Sewer56
Copy link
Contributor Author

Sewer56 commented Jan 7, 2026

@malhashemi If you wish to, fire away :p
Should have write perms.

@rekram1-node
Copy link
Collaborator

ignore failures, ill patch it and merge

@malhashemi
Copy link
Contributor

malhashemi commented Jan 7, 2026

@rekram1-node so do I leave it to you then? just as mentioned we could use the fact that we are exposing the agent now to the tool to make the description filtration self contained within the task tool. Also welcome back I hope you partied hard enough in that wedding, new issues been raining haha

@malhashemi
Copy link
Contributor

malhashemi commented Jan 7, 2026

Also to let you know we cannot @ a subagent if we select a primary that is denying it as of now. Looks like Dax said he doesn't want that behavior but he implemented it himself with that permission PR. As usual keep fixing what Dax breaks :))

@Sewer56
Copy link
Contributor Author

Sewer56 commented Jan 7, 2026

Also to let you know we cannot @ a subagent if we select a primary that is denying it as of now.

With one of the recent merges from dev?
This did work when I tested it 2 days ago with my last push; I wonder if something has changed since.

Edit: Just confirmed.

It should work if the subagent is not hidden. It's one of the things I've had to retest a lot when there have been conflicts with dev.

Invokable by user:

image

But not in system prompt:

image

@malhashemi
Copy link
Contributor

With one of the recent merges from dev? This did work when I tested it 2 days ago with my last push; I wonder if something has changed since.

Oh haven't tested this with your branch yet, but on dev yeah you cannot @ them now. basically it acts as the primary agent is the one invoking the sub.

@Sewer56
Copy link
Contributor Author

Sewer56 commented Jan 7, 2026

Oh yeah, you can't do that on dev, haha.
I had to fix it for this PR, yup.

@rekram1-node
Copy link
Collaborator

rekram1-node commented Jan 7, 2026

I tested quite a bit I think everything works as expected (in this PR)

@rekram1-node
Copy link
Collaborator

waiting for ci to finish then i click the shiny button

@malhashemi
Copy link
Contributor

So no refactor? If it works do not mess with it haha?

@rekram1-node
Copy link
Collaborator

So no refactor? If it works do not mess with it haha?

I think the code could be cleaned up a little cause there is already a flag to bypass checks in task tool but the # of lines that arent in test file is like 100 and Idrc its not a huge headache to clean up later

@rekram1-node rekram1-node merged commit fd7b7ea into anomalyco:dev Jan 7, 2026
2 checks passed
@malhashemi
Copy link
Contributor

malhashemi commented Jan 7, 2026

What matters it is working :D thanks @Sewer56 for all the work you put into this, thanks @rekram1-node for merging <3

@rekram1-node
Copy link
Collaborator

yes definitely thanks to @Sewer56 i think they may be a saint, given the amount of patience they had for me xD

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

Labels

None yet

Projects

None yet

6 participants