[BUG] Skill with context: fork can infinitely recurse: forked subagent re-dispatches its own Skill

Resolved 💬 4 comments Opened May 2, 2026 by srid Closed May 19, 2026

Summary

A skill declared with context: fork can enter an infinite recursion under Sonnet 4.6 when invoked via the Skill tool. Each forked subagent's only action is to re-invoke Skill(skill="<self>", args=<verbatim>) instead of executing the skill body. The harness has no re-entry guard, so each call spawns another fork with an identical first user message → identical interpretation → identical dispatch. Stable fixed point, terminates only on manual kill or rate limits.

Caught in the wild on srid/agency's /do workflow (repro with timeline and logs; follow-up root-cause analysis). Burned ~5 minutes and 102+ Sonnet subagents before manual intervention.

Repro

  1. Define a skill with context: fork whose body is a long methodology specification (top-level # <Name>: <tagline> header, descriptive third-person prose, layered ## Layer N sections, ## Output Format, ## Anti-patterns).
  2. Invoke it via the Skill tool.
  3. With agent: general-purpose on the fork (or any agent that exposes the Skill tool) and the fork running on Sonnet 4.6, the forked subagent's first action is Skill(skill="<self>", args=<verbatim ARGUMENTS block>) ~half the time.
  4. Each recursion is identical, so the harness keeps spawning forks at ~one every 5s.

Concrete failing case: agency commit d2aff43 (now reverted), .apm/skills/lowy/SKILL.md and .apm/skills/hickey/SKILL.md, both ~10–16 KB methodology specs.

Evidence

In the failing session (73c3a79d-52c8-42bb-988b-5ffefb4e7e08), every subagents/agent-*.jsonl for the looping lowy skill contained exactly two records:

  1. user: Base directory for this skill: /home/srid/code/quickshare/.claude/skills/lowy\n\n# Lowy: Volatility-Based Decomposition Review\n\n… followed by the ARGUMENTS: block.
  2. assistant: a single tool_use of Skill with input.skill = "lowy" and input.args equal to the same ARGUMENTS: block.

No Read, no Bash, no Grep — never any analysis. The subagent's only action was to re-invoke its own skill. hickey (invoked the same way moments earlier on the same diff) completed normally in ~75s. Same mechanism, prompt-shape coin flip — Sonnet 4.6 wins the dispatch interpretation roughly half the time on bodies of this shape.

Root cause

Prompt-shape collision in context: fork execution. When the harness forks for a context: fork skill, it builds the new subagent's first user message as:

Base directory for this skill: <skill_dir>

<entire SKILL.md body>

ARGUMENTS: <args>

That payload is shaped identically to what a main-turn agent receives when a user posts a skill spec asking for it to be dispatched (header + body + ARGUMENTS block). The forked subagent's system prompt comes from the agent type (here general-purpose), which exposes the Skill tool. The currently-running skill is not stripped from the toolset.

Sonnet 4.6 then pattern-matches "skill spec in user slot → dispatch" (a shape it has seen extensively in training) and emits Skill(skill="<self>", args=<verbatim>) as its first action — never reading the body as instructions. The harness has no re-entry guard, so the next fork sees an identical first user message → identical dispatch. Stable fixed point.

Why some skills trigger this and most don't

The four conditions (context: fork, agent that exposes Skill, model that pattern-matches, body shape) are necessary but not sufficient. What tips a specific skill over:

  • Long methodology-spec structure — layered ## Layer N headings, ## Output Format, ## Anti-patterns. ~10–16 KB. Pattern-matches "skill specification document," not "runbook."
  • Descriptive third-person prose ("This skill evaluates…"), not imperative second-person ("You are evaluating…"). The first frames the body as a spec to dispatch; the second frames it as steps to follow inline.
  • Self-referential Skill invocations baked into the body ("invoke /fact-check on your own output"). Primes the dispatch frame before the model has read past the first paragraph.
  • # <SkillName>: <tagline> top-level header — canonical skill-index shape, reinforces "spec" reading from the first token.
  • Structured ARGUMENTS (# Context\n\n… from callers, not a free-form sentence) — strengthens "spec + ARGUMENTS dispatch payload" framing.

Most short, imperative context: fork skills read as orders, not specs, so they execute inline.

Wrapper indirection avoids the loop (current workaround)

The pre-revert agency code worked because skills were invoked via a 9-line wrapper subagent: Agent(subagent_type="lowy") → wrapper subagent (system prompt = "You are the lowy reviewer. Invoke the lowy skill via the Skill tool…", user slot = caller's args) → wrapper fires one Skill(skill="lowy") → executes inline within the wrapper's already-forked context.

Works because the wrapper rewrites the user-slot text so the dispatch shape never appears in any fork. The methodology body sits in the system prompt of the inline execution, not the user slot. Costs: an extra wrapper file per reviewer and redundant frontmatter for model selection.

Suggested fixes (any one breaks the loop)

  1. Strip the currently-running skill from the forked-exec subagent's Skill toolset. A skill that's already executing shouldn't be re-callable from inside its own execution. Most surgical; no model-behavior dependency.
  2. Re-entry guard in the harness. When Skill(name=X) is called from a subagent that was itself spawned to run skill X, refuse or return a cached no-op result. Defense in depth — catches arbitrary cycle lengths, not just self-loops.
  3. Reframe the fork's first user message. Replace Base directory for this skill: <path>\n\n<body>\n\nARGUMENTS: <args> with framing that doesn't pattern-match as "user is asking me to invoke this skill" — e.g., prepend > You are executing this skill. Do not call Skill(skill="<self>"). Run the steps below directly. Less reliable than (1)/(2) since model behavior drifts; suitable only as belt-and-suspenders.

(1) is the cleanest and addresses the root cause without prompt engineering. (2) is the strongest guarantee.

Environment

  • Driver / main thread: claude-opus-4-7
  • Forked subagents: claude-sonnet-4-6, agentType: general-purpose, isSidechain: true
  • Claude Code CLI: April 2026
  • Reproduces on macOS and Linux

Related

View original on GitHub ↗

This issue has 4 comments on GitHub. Read the full discussion on GitHub ↗