Repository navigation
Description
Summary
Claude Code records a plugin skill's Skill call with the plugin-qualified name, for example {"skill": "toolbox:doc-truth"}. harvest._skill_names_from_content keeps the name as written (test_sleep_skill_harvest.py pins Example-Skill:v2), so the fan-out looks for a skill called toolbox:doc-truth.
resolve_skill then looks for a directory with that literal name in every root. The plugin roots that skill_search_roots discovers (<cache>/<marketplace>/<plugin>/<version>/skills, from #185 and 109bafb) contain doc-truth/, so a plugin skill from a real transcript always comes back missing. The resolver tests use bare names in plugin roots (chrome-devtools under chrome-devtools-mcp/<version>/skills), which is not what Claude Code writes.
Repro
On main at 343db22, with a claude_home holding plugins/cache/jay-skills/toolbox/0.6.4/skills/doc-truth/SKILL.md:
roots = skill_search_roots(cfg) # includes .../toolbox/0.6.4/skills
resolve_skill("toolbox:doc-truth", roots).status # 'missing'
resolve_skill("doc-truth", roots).status # 'found'
_skill_names_from_content([{"type": "tool_use", "name": "Skill",
"input": {"skill": "toolbox:doc-truth"}}]) # ['toolbox:doc-truth']End to end on 79124b3 (skill_resolver.py is unchanged since), a dry-run --source claude with multi_skill_fanout on a real session reported the group as skipped, "no SKILL.md for this skill in the configured skill roots". --skill-root can't work around it, because no directory has a colon in its name.
On one machine's Claude Code history: 264 Skill calls, 138 of them plugin-qualified, and 129 of those have <skill>/SKILL.md in the root the resolver picks for that plugin. None of the 138 resolve today.
Why it needs more than a resolver change
If the name did resolve, staging would still refuse it. staging._safe_skill_name rejects : because the name becomes proposed_SKILL.<name>.md and Windows can't store it. skill_proposal_rows and _adopt_live_target_ok also require the live file's parent directory to equal the skill name. So the fix depends on what name a plugin skill is staged and adopted under, and I'd rather you pick that.
Options
- Resolve
plugin:skillin that plugin's root only, and stage under the directory name. Return the plugin roots keyed by plugin, send a qualified name only to its plugin's root, and send a bare name to every root as today. Reports keeptoolbox:doc-truth; the proposal is staged and adopted asdoc-truth(adopt --skill doc-truth). If two plugins'doc-truthshow up in one night,skill_proposal_rowsalready refuses the duplicate. The resolver still never guesses between roots. - Same resolution, staged under an encoded name such as
toolbox--doc-truth, soadopt --skillnames stay unique across plugins.skill_proposal_rowsand_adopt_live_target_okwould need a new rule, since today they tie the staged name to the directory name. - Strip the prefix at harvest and resolve the bare name everywhere. This is the smallest change, but a bare name can then match an unrelated skill. If a plugin's copy isn't in the cache,
engineering:code-reviewwould resolve to~/.claude/skills/code-reviewand the proposal would be staged against that file. I wouldn't do this one.
I lean to option 1. Happy to send a PR with tests once you've picked the naming: a qualified name resolves only in its plugin's root, a same-named skill elsewhere is ignored, staging and adopt --skill work under the bare name, and duplicate bare names across plugins are still refused.
Activity
Yif-Yang commentedon Nov 4, 2026
Thanks for the precise diagnosis. #339 proposes a fix: it resolves
plugin:skillnames only within the matching plugin cache root, and stages proposals under the live directory name. It is in our review queue, and we'll update here when it lands.