Repository navigation
Task permission: a wildcard deny with named allows empties the Task tool #777
Description
Symptom
When an agent's permission.task map is a catch-all deny plus named allows, OpenCode's Task tool disappears from that agent's toolset entirely — the agent cannot dispatch the very subagent it names.
# .opencode/agent/lead.md
permission:
task:
"*": deny
my-scout: allow
my-builder: allowThe agent runs with no task tool at all. Nothing else about the agent is affected.
Cause
denyTaskRoutingToAgents() in src/agents/permissions.ts reads the agent's own permission.task map and, when it finds a literal "*" key with effect deny, drops that key and re-appends it after the agent's own rules. OpenCode evaluates permission rules with the last matching rule winning, so the re-appended wildcard overrides every named allow and the Task tool is left with zero candidates.
The plugin's intent is sound — keep the Task tool hidden for an agent that must not dispatch at all — but the two intents are indistinguishable from the map alone:
task: { "*": deny }— deny all dispatchtask: { "*": deny, "x": allow }— deny all dispatch exceptx(the common deny-by-default shape)
The second is treated as the first, and the agent's own ordering (catch-all first, named allows after — the correct OpenCode shape) is inverted.
Reproduction
Two agents, identical except for the catch-all pattern, run with opencode run --agent <name>:
# agent A
permission: { task: { "*": deny, "probe-target": allow } }
# agent B
permission: { task: { "**": deny, "probe-target": allow } }- Agent A — the model reports it has no task tool.
- Agent B — the model dispatches
probe-targetsuccessfully, and a dispatch of any other agent is refused withuser permission rule denied task pattern "**" (only probe-target allowed).
The resolved rule list for agent B, showing the plugin's appended internal denies are harmless because they come after the agent's own allows:
task: {"*": allow} # global config
task: {"**": deny} # agent's own catch-all
task: {probe-target: allow} # agent's own named allow
task: {dreamer: deny} … # plugin's internal denies
Measured on OpenCode 1.19.4 with @cortexkit/opencode-magic-context 0.45.0.
Suggested fix
Only re-append the ambient wildcard when it is the only rule in the map — i.e. when there is no named allow for it to override:
const hasOnlyWildcard =
hasAmbientWildcardDeny && Object.keys(configuredTask).every((id) => id === "*");With that condition, keep today's behaviour for task: { "*": deny } (the tool stays hidden), and otherwise leave the ambient wildcard in its original position so the agent's own ordering survives.
The same reasoning applies to the other named denies the plugin appends: they are appended last today, so they already win over an agent's own allows for the same id — which is the desired behaviour for the plugin's own agents, but worth keeping in mind if an agent ever names one of them deliberately.
Workaround
Until this is fixed, write the catch-all as "**" instead of "*". OpenCode's pattern matcher treats a single asterisk as "zero or more of any character", so "**" matches every agent id exactly like "*", and the plugin's literal-"*" check does not see it.
Activity
magic-alfonso commentedon Oct 24, 2026
Thanks for the precise trace. Confirmed on current master, not only on 0.45.0.
denyTaskRoutingToAgents()inpackages/plugin/src/agents/permissions.tsdrops a literal"*": "deny"from the agent'spermission.taskmap and re-inserts it after our internal denies (lines 135–151). OpenCode resolves task permissions with the last matching rule, so with{ "*": deny, "my-scout": allow }the re-inserted wildcard wins over your named allows and the Task tool disappears. The intent was only to hide the tool for an agent that may not dispatch at all, and the code can't tell that apart from the deny-by-default-with-exceptions shape you're using.Your workaround is the right one until the fix ships: write the catch-all as
"**", which matches every agent id the same way but isn't the literal"*"the plugin looks for.We're queuing this behind work already in progress, so the fix may take a little while. The direction is what you suggest: only move the wildcard when it's the agent's only task rule, and keep the agent's own ordering otherwise. We'll post here when it ships.
DreamchaserLG commentedon Oct 26, 2026
Root cause confirmed in
denyTaskRoutingToAgents(): it drops the literal"*": "deny"from the agent's ownpermission.taskmap and re-appends it after the internal denies, and since OpenCode resolves with the last matching rule, the re-appended wildcard clobbers every named allow in the deny-by-default-with-exceptions shape.I've opened a draft PR implementing the direction from your reply (move the wildcard only when it stands alone), with one refinement: the "stands alone" check exempts our own appended agent-id denies, because the strict
every(id => id === "*")predicate from the issue breaks the existing re-registration idempotence test — our merged output is exactlyinternal denies + wildcard, so it must re-merge to itself.magic-alfonso commentedon Oct 26, 2026
Reopening for now. The fix from @DreamchaserLG's pull request 787 is merged on master, but it hasn't shipped yet: a closing line in the pull request's commit message closed this issue when the merge was pushed. It ships in 0.49.24, and this issue closes with the release note.
fix(agents): keep named task allows winning over the ambient wildcard…
magic-alfonso commentedon Oct 27, 2026
Fixed in 0.49.24. Thanks @selfagency for the report and @DreamchaserLG for the fix in #787.
A task permission written as a wildcard deny followed by named allows (for example
"*": "deny"then"my-agent": "allow") now keeps the user's own order, so the named agents stay allowed and the Task tool appears again. Magic Context moves the wildcard deny to the end only when it is the sole rule, so "deny everything" still hides the tool.Update the plugin and restart OpenCode to pick it up.
feat(mcp): a delegation needs an owner and a return condition