[BUG] Skill with context: fork can infinitely recurse: forked subagent re-dispatches its own Skill
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
- Define a skill with
context: forkwhose body is a long methodology specification (top-level# <Name>: <tagline>header, descriptive third-person prose, layered## Layer Nsections,## Output Format,## Anti-patterns). - Invoke it via the
Skilltool. - With
agent: general-purposeon the fork (or any agent that exposes theSkilltool) and the fork running on Sonnet 4.6, the forked subagent's first action isSkill(skill="<self>", args=<verbatim ARGUMENTS block>)~half the time. - 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:
user:Base directory for this skill: /home/srid/code/quickshare/.claude/skills/lowy\n\n# Lowy: Volatility-Based Decomposition Review\n\n…followed by theARGUMENTS:block.assistant: a singletool_useofSkillwithinput.skill = "lowy"andinput.argsequal to the sameARGUMENTS: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 Nheadings,## 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
Skillinvocations baked into the body ("invoke/fact-checkon 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)
- Strip the currently-running skill from the forked-exec subagent's
Skilltoolset. A skill that's already executing shouldn't be re-callable from inside its own execution. Most surgical; no model-behavior dependency. - Re-entry guard in the harness. When
Skill(name=X)is called from a subagent that was itself spawned to run skillX, refuse or return a cached no-op result. Defense in depth — catches arbitrary cycle lengths, not just self-loops. - 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
- srid/agency#138 — original repro
- srid/agency#139 — revert PR with root-cause writeup
- anthropics/claude-code#30256 — inverse failure mode (premature exit on nested Skill from
context: fork); shares the surface - anthropics/claude-code#46654, #54892 — other
context: forktoolset issues
This issue has 4 comments on GitHub. Read the full discussion on GitHub ↗