Forked skill invoked from a subagent returns no task_id or output path, and its completion notification routes to the parent session
Summary
When a subagent invokes a skill that runs forked (Skill({skill: "code-review", args: "max <pr>"})), it has no supported way to receive that skill's result. The tool result carries neither a task_id nor an output path, and the child's completion <task-notification> is routed to the parent session rather than to the subagent that made the call. The caller is left holding only a name.
This breaks the retrieval contract that TaskOutput's own deprecation notice states:
Background tasks return their output file path in the tool result, and you receive a <task-notification> with the same path when the task completes.
A forked skill honors neither half of that, and a subagent caller can't fall back to the notification either, because the notification goes somewhere it can't see.
What the caller actually receives
Skill "code-review" launched (forked execution, running in the background).
Running in the background as @code-review
That's the entire result. No task_id, no path.
Why it's a problem
The subagent had been instructed to run the review and write its findings to a file. With no in-band channel it did the only thing left: polled ListAgents on a ladder of blocking python3 -c "time.sleep(...)" calls, then located the child's task_id by listing the tasks/ directory on disk and parsed the child's agent-<id>.jsonl transcript to recover the findings.
That recovery path is one TaskOutput explicitly warns against:
For local_agent tasks: use the Agent tool result directly. Do NOT Read the .output file — it is a symlink to the full subagent conversation transcript (JSONL) and will overflow your context window.
So the documented guidance is "use the Agent tool result," but a forked skill never produces one for its caller. It worked here, but only by luck of a small transcript, and it cost ~20 minutes of wall clock — the poll ladder overshot the child's actual completion by 7 minutes sitting inside a sleep(480).
SendMessage({to: "code-review", ...}) does appear to work as a workaround, since the forked agent is named and reachable via ListAgents. But nothing in the Skill tool result suggests it, and it isn't obviously the intended retrieval mechanism for a skill result.
Reproduction
- From a
Workflowscript (or anyAgentdispatch), spawn a subagent whose prompt tells it to invoke a skill that forks — the built-incode-reviewatmaxeffort does — and to write the result to a file. - Observe the subagent's tool result: a name only.
- Observe the child's completion notification arriving in the parent session's context, not the subagent's.
The fork is identifiable on disk by the agent-<id>.forked-skill.json / .forked-skill.marker.json marker files.
What would fix it
Open to whichever of these fits the architecture best:
- Return a
task_idand output path in the forked-skill tool result, matching every other background task type. - Route the completion notification to the calling agent when the caller is itself a subagent (or deliver it to both).
- Give
Skill()a way to request blocking/inline execution, so a caller that needs the result in-band can say so up front rather than discovering after the fact that it won't get one. - Failing all of the above: document
SendMessageas the supported way to retrieve a forked skill's result, and say so in theSkilltool result itself.
Any one of the first three would remove the need for transcript parsing.
Environment
- Claude Code v2.1.246
- macOS (Darwin 25.5.0), arm64
- Caller was a
Workflow-spawned subagent; forked skill was the built-incode-reviewatmaxeffort
This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗