Skip to content

tools: task: true in a subagent's frontmatter enables unbounded recursive self-spawning #25681

Closed as not planned

Description

@tim-mohrbach-ikigai

Summary

When a custom subagent has tools: task: true in its frontmatter, it can spawn a copy of itself as a child session, which spawns another copy, and so on without limit. There is no depth cap, no cycle detection, and no user-visible warning. The result is a silent runaway chain that consumes tokens for as long as the process runs.

This is a second distinct trigger path for the same underlying mechanism described in #17722. That issue requires "task": "allow" in the global opencode.json permission block. This one is triggered purely by the agent's own frontmatter — a natural and expected way to grant a subagent delegation ability — and is not covered by the existing docs warning in PR #17724.

Steps to reproduce

  1. Create a custom subagent with tools: task: true in its frontmatter:
---
description: Explores codebases
mode: subagent
tools:
  read: true
  glob: true
  grep: true
  bash: true
  task: true   # ← trigger
---
  1. Invoke the subagent with a task it cannot fully complete in one session (e.g. "find the X configuration in this codebase").
  2. The subagent, unable to finish, spawns a new instance of itself via the task tool with the same prompt.
  3. Each child does the same. The chain grows indefinitely.

What happens under the hood

normalize() in config/agent.ts translates tools: { task: true } into permission: { task: "allow" }. When task.ts creates a child session it checks:

const canTask = next.permission.some((rule) => rule.permission === "task")

Because canTask is true, the task tool is not stripped from the child's permission set:

...(canTask ? [] : [{ permission: "task", pattern: "*", action: "deny" }]),

So every child inherits task: allow from its parent and can spawn further children. There is no depth counter, no cycle detection on prompt content, and no cap anywhere in the call chain.

This is mechanically identical to the #17722 case — canTask = true is the common condition — but reached via a different config path that PR #17724 does not cover.

Observed impact

A subagent with this configuration triggered a chain of 612 nested sessions over 73 minutes — roughly one new session every 7 seconds. The chain was not stopped by any OpenCode safeguard. It stopped only because the API credit limit was exhausted. Without that external constraint it would have continued indefinitely.

Why PR #17724 doesn't cover this

PR #17724 is docs-only and its warning is scoped to "task": "allow" in the global permission block. It explicitly states:

"You do not need an explicit "task" permission for primary agents to spawn subagents — the default "*": "allow" already covers this."

That statement is correct for primary agents but misleading for subagent frontmatter, where tools: task: true is the natural way to enable delegation. A user following the docs would have no reason to suspect this triggers the same recursion trap.

Proposed fixes

Option A — Depth limit (addresses both this issue and #17722 at the root):
Track nesting depth via the parent_id chain in the session DB and refuse to spawn in task.ts if depth >= MAX_SUBAGENT_DEPTH (e.g. 10). This is the only option that provides a hard runtime guarantee regardless of how the permission was granted.

Option B — Strip task from subagent children by default:
Change the default so task is always stripped from child sessions unless the parent explicitly re-grants it. canTask becomes opt-in rather than opt-out. This would be a breaking change for users who rely on deliberate multi-level delegation.

Option C — Extend PR #17724's docs warning:
Add a note covering the tools: task: true frontmatter pattern alongside the existing global config warning. Near-term mitigation only — no runtime protection.

Option A is the cleanest fix and would close both this issue and #17722 with a single code change.

Related

Activity

  1. github-actions commented on May 4, 2026

    @github-actions
    Contributor

    This issue might be a duplicate of existing issues. Please check:

    The core mechanism and proposed fixes (depth limit, stripping task from child sessions) appear identical to #18100. You may want to add your findings there as additional context rather than tracking this separately.

  2. tim-mohrbach-ikigai commented on May 4, 2026

    @tim-mohrbach-ikigai
    Author

    Closing as a duplicate of #18100, which covers the same underlying framework gap (no recursion depth limit on the Task tool).

    The additional findings from this issue — the tools: task: true frontmatter trigger path, the normalize() code trace, and the 612-session impact data — have been added as a comment on #18100 so they are available to whoever implements the fix.

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

Metadata

Metadata

Assignees

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