context:fork skill executions inherit the caller's StructuredOutput tool; forks ending on it return a generic success stub and their output is silently lost
Summary
When a skill with context: fork frontmatter is invoked via the Skill tool from a subagent that has a StructuredOutput tool (e.g. any Workflow agent(..., {schema}) wrapper), the caller's StructuredOutput tool leaks into the forked execution's tool inventory. If the fork finishes by calling that leaked tool (which sets toolEndsTurn: true), the fork ends with no trailing assistant text, and the Skill relay — which appears to harvest only trailing text — returns a generic success stub to the caller:
Skill "<name>" completed (forked execution).
Result:
Skill execution completed
The fork's real answer is silently discarded. No error is surfaced anywhere: the workflow journal records a schema-valid (empty/fallback) result, the Skill tool result reads as success, and the fork's transcript shows minutes of genuine work ending in a StructuredOutput call whose payload is never delivered.
Environment
- Claude Code 2.1.204, macOS (darwin 25.5.0)
- Skill: a plugin skill with
context: fork(observed with a codex-CLI wrapper skill, but nothing about it is skill-specific)
Reproduction shape
- Define a plugin skill with
context: fork. - Run a Workflow whose script calls
agent(prompt, {schema: SOME_SCHEMA})— the wrapper subagent gets aStructuredOutputtool to return its result. - The wrapper's prompt tells it to invoke
Skill(<the forked skill>)with a full self-contained question inargs. - The forked execution runs normally (its transcript shows real tool use for minutes), but — seeing
StructuredOutputin its inventory — often "returns" its final answer by calling it instead of emitting plain text. - The wrapper receives the stub above instead of the answer.
Evidence from one production run
A review workflow fired 18 Skill(<forked skill>) invocations from schema-bound wrapper agents (5 finder wrappers + 6 refuter wrappers, some retrying):
- 18/18 forks spawned successfully and ran 1–6 minutes each (transcripts on disk,
spawnDepth: 2). - 15/18 forks ended their turn on a
tool_resultforStructuredOutput(toolEndsTurn: true) with no assistant text after it → all 15 callers received the byte-identical 78-char stub. - 3/18 forks ended with plain assistant text → all 3 callers received the real answer. (Two of those had used
ReportFindings, which does not end the turn, so they kept talking afterward.) - The correlation is 100%. No rate-limit/5xx/API errors anywhere in the window; failures and successes interleaved at the same concurrency, so this is not load-related.
- One discarded payload was a complete, correct refutation verdict — the fork called
StructuredOutputwith it 2.3s before the wrapper received the empty stub. The wrapper then submitted a fallback "unavailable" verdict, silently degrading the review.
Expected behavior
Either (ideally both):
- Tool isolation: a
context: forkexecution should not inherit the calling agent'sStructuredOutput(or other caller-scoped return tools) — the fork has no legitimate way to use it, and its presence actively invites the fork to "return" through it. - Relay robustness: if a fork ends its turn on a tool call with no trailing text, the Skill relay should either deliver the last tool payload or return an explicit error — anything but a generic success stub that masks the data loss.
Workaround
Don't nest forked skills inside schema-bound subagents: we now ship the skill's recipe as a plugin agent definition and spawn it directly (agentType/subagent_type), which removes the inner fork entirely.