claude-code - ✅(Solved) Fix [BUG] Skill with context: fork can infinitely recurse: forked subagent re-dispatches its own Skill [1 pull requests, 2 comments, 2 participants]

Official PRs (…)
ON THIS PAGE

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
anthropics/claude-code#55592Fetched 2026-05-03 04:49:25
View on GitHub
Comments
2
Participants
2
Timeline
6
Reactions
0
Author
Timeline (top)
labeled ×3commented ×2cross-referenced ×1

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.

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.

Fix Action

Fix / Workaround

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.

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.

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.

PR fix notes

PR #153: hickey, lowy: move model: sonnet from wrappers into skills

Description (problem / solution / changelog)

model: sonnet now lives in each reviewer's SKILL.md, not in the wrapper agent. This fixes opencode users on non-Sonnet providers who were hitting ProviderModelNotFoundError (#119) because the wrapper hardcoded a model their harness couldn't honor. Moving the field into the skill itself preserves Claude Code's cheap-Sonnet review path while letting opencode/Codex fall through to the active model — model: is a Claude Code extension, not part of the Agent Skills standard, so non-Claude-Code harnesses just ignore it.

Per-harness behavior

HarnessBeforeAfter
Claude CodeWrapper forced SonnetWrapper inherits parent; SKILL.md model: sonnet overrides — review on Sonnet
opencodeWrapper forced Sonnet → ProviderModelNotFoundErrorWrapper inherits active model; SKILL.md model: ignored → active model
CodexSame as opencodeSame as opencode

What changes

  • Drop model: sonnet from .apm/agents/{hickey,lowy}.md
  • Add model: sonnet to .apm/skills/{hickey,lowy}/SKILL.md
  • Drop --review-model=<opus|sonnet|haiku> from /do and /talk (argument hint, parse line, model-override sections). With the model now in the skill, any caller-supplied model on the Agent tool gets overridden by the skill frontmatter, so the flag is a no-op on Claude Code.
  • Name opencode's task tool alongside Claude Code's Agent and Codex's sub-agent tool in /do's hickey+lowy step (closes the docs gap from #119)
  • README sentence updated to match

Why the wrappers stay

They're still load-bearing for anthropics/claude-code#55592. Calling Skill(skill="lowy") directly on context: fork skills with spec-shaped bodies puts the SKILL.md body into the forked subagent's user slot, which Sonnet 4.6 pattern-matches as "user is asking me to dispatch this skill" and recursively invokes itself. Reverted in #139 after the bug burned 102+ subagents in a single run. The 9-line wrappers reframe the user-slot text so the dispatch shape never appears. They go away once #55592 ships fix (1) "strip currently-running skill from forked-exec toolset" or fix (2) re-entry guard.

Verify before relying on this

srid noted in #119 that Claude Code's model: skill extension hasn't been confirmed to apply when the skill is invoked from inside an Agent-spawned wrapper subagent (vs. the documented main-turn case). Quick smoke test: run /do on a small diff, then check ~/.claude/projects/<repo>/<sid>/subagents/agent-*.jsonl and confirm the hickey/lowy subagents show model: claude-sonnet-4-x. If Claude Code instead uses the wrapper's inherited Opus model, the cost story breaks and we'd need a different mechanism (e.g. give the wrapper a Claude-Code-only model: via APM-host-conditional compilation).

Lost: --review-model=opus for one-off deeper passes

The flag worked by passing model: to the Agent tool, which would override the wrapper's frontmatter. With SKILL.md's model: sonnet now overriding any caller-supplied model on Claude Code, the flag would be a silent no-op. Removed cleanly. If a deeper one-off pass is ever needed, the cleanest replacement would be a separate Opus-pinned wrapper skill — but the use case hasn't come up enough to build that yet.

Closes #119.

Generated by Claude Code (model claude-opus-4-7).

Changed files

  • .apm/agents/hickey.md (modified, +0/-1)
  • .apm/agents/lowy.md (modified, +0/-1)
  • .apm/skills/do/SKILL.md (modified, +5/-6)
  • .apm/skills/hickey/SKILL.md (modified, +1/-0)
  • .apm/skills/lowy/SKILL.md (modified, +1/-0)
  • .apm/skills/talk/SKILL.md (modified, +2/-2)
  • README.md (modified, +1/-1)

Code Example

Base directory for this skill: <skill_dir>

<entire SKILL.md body>

ARGUMENTS: <args>
RAW_BUFFERClick to expand / collapse

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

extent analysis

TL;DR

The most likely fix for the infinite recursion issue in skills declared with context: fork is to strip the currently-running skill from the forked-exec subagent's Skill toolset.

Guidance

  • Identify skills that match the conditions for the prompt-shape collision: context: fork, long methodology-spec structure, descriptive third-person prose, self-referential Skill invocations, and structured ARGUMENTS.
  • Apply one of the suggested fixes: strip the currently-running skill from the forked-exec subagent's Skill toolset, add a re-entry guard in the harness, or reframe the fork's first user message.
  • Verify the fix by invoking the skill via the Skill tool and checking for infinite recursion.
  • Consider using a wrapper indirection as a temporary workaround, which rewrites the user-slot text to avoid the dispatch shape.

Example

No code snippet is provided as the issue is related to the interaction between the Skill tool and the harness, and the fix involves modifying the toolset or the harness.

Notes

The issue is specific to Sonnet 4.6 and may not occur in other versions. The suggested fixes may have different trade-offs, and the choice of fix depends on the specific use case and requirements.

Recommendation

Apply the first suggested fix: strip the currently-running skill from the forked-exec subagent's Skill toolset. This fix is the cleanest and addresses the root cause without prompt engineering.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING

claude-code - ✅(Solved) Fix [BUG] Skill with context: fork can infinitely recurse: forked subagent re-dispatches its own Skill [1 pull requests, 2 comments, 2 participants]