Derive extension source from the tool's install manifest, not the enclosing .git
Summary
HarnessKit infers an extension's source by walking up to the nearest enclosing .git (scanner::detect_source). Whenever the files live inside the user's own dotfiles repo (e.g. ~/.claude kept under git), everything beneath it is mis-attributed to that dotfiles remote, regardless of where it actually came from. This is one root cause with several faces:
| Face |
Mechanism |
Status |
Symlinked skill (e.g. ~/.claude/skills/tdd → ~/.agents/skills/tdd) forks into a dotfiles-repo group |
detect_source walks the symlink's textual parents to ~/.claude/.git |
Fixed in #88 (canonicalize + self-heal) |
install_meta itself stamped git+dotfiles by the source backfill |
grouping then prefers the polluted install_meta.url |
Partly addressed (#76 frontend, #88 skill heal) |
Plugins cached under ~/.claude/plugins/cache/<marketplace>/<plugin>/ mis-attributed to the dotfiles repo |
real directories inside ~/.claude/.git; no symlink to resolve |
Not fixed |
Shared skills root (~/.agents) kept under git, or a marketplace skill copied as a real dir into ~/.claude |
same enclosing-.git inference |
Not fixed |
#76 and #88 patch individual faces at the symptom level (frontend grouping; the symlink subset). They cannot fix plugins (no symlink) or the ~/.agents-under-git case, because the source is inferred rather than read from the authoritative record.
Reproduce
- Keep
~/.claude under a personal dotfiles git repo (common backup setup).
- Install Claude plugins from any marketplace (they cache under
~/.claude/plugins/cache/<marketplace>/...).
- Open Extensions, group by source.
Expected: each plugin attributed to its marketplace's upstream repo (recorded in ~/.claude/plugins/known_marketplaces.json).
Actual: every plugin attributed to the user's dotfiles repo, collapsing N unrelated marketplaces into one bogus group.
The analogous skill case: with ~/.agents under git, every CLI-installed skill shows the ~/.agents remote instead of its real source from ~/.agents/.skill-lock.json.
Root fix
Tool-managed extensions should take their source from the tool's own install manifest, falling back to detect_source only for genuinely git-cloned extensions with no manifest entry:
- Skills →
<root>/.skill-lock.json (the skills CLI lockfile), keyed by skill name: source (owner/repo) + sourceUrl.
- Plugins →
~/.claude/plugins/installed_plugins.json (<plugin>@<marketplace>) + ~/.claude/plugins/known_marketplaces.json (marketplace → owner/repo).
This subsumes the symptom-level patches: with the authoritative source in hand there is no polluted source.url/install_meta to outrank, and the symlink canonicalize/heal in #88 become defensive rather than load-bearing.
Scope of the proposed PR
scanner::scan_skill_dir: after detect_source, override with the lockfile entry when present (cached per lockfile, one read per scanned dir).
PluginEntry: new source_url field; Claude's adapter fills it from known_marketplaces.json; scanner::scan_plugins prefers it over .git detection. Other adapters leave it None (unchanged behavior).
- Regression tests: lockfile beats a populated enclosing repo (and a sibling skill absent from the lock still falls back); a plugin is attributed to its marketplace repo.
Deferred / follow-up
- Codex and Copilot also parse marketplace-based plugins; they can adopt the same
source_url resolution later (kept None here to stay surgical).
- Once manifest-based resolution lands, the store's git-source backfill (which stamps
install_meta from the inferred enclosing .git) can be narrowed or retired.
Depends on #88 (built on its canonicalize in scan_skill_dir). PR incoming.
Derive extension source from the tool's install manifest, not the enclosing
.gitSummary
HarnessKit infers an extension's source by walking up to the nearest enclosing
.git(scanner::detect_source). Whenever the files live inside the user's own dotfiles repo (e.g.~/.claudekept under git), everything beneath it is mis-attributed to that dotfiles remote, regardless of where it actually came from. This is one root cause with several faces:~/.claude/skills/tdd→~/.agents/skills/tdd) forks into a dotfiles-repo groupdetect_sourcewalks the symlink's textual parents to~/.claude/.gitinstall_metaitself stampedgit+dotfiles by the source backfillinstall_meta.url~/.claude/plugins/cache/<marketplace>/<plugin>/mis-attributed to the dotfiles repo~/.claude/.git; no symlink to resolve~/.agents) kept under git, or a marketplace skill copied as a real dir into~/.claude.gitinference#76 and #88 patch individual faces at the symptom level (frontend grouping; the symlink subset). They cannot fix plugins (no symlink) or the
~/.agents-under-git case, because the source is inferred rather than read from the authoritative record.Reproduce
~/.claudeunder a personal dotfiles git repo (common backup setup).~/.claude/plugins/cache/<marketplace>/...).Expected: each plugin attributed to its marketplace's upstream repo (recorded in
~/.claude/plugins/known_marketplaces.json).Actual: every plugin attributed to the user's dotfiles repo, collapsing N unrelated marketplaces into one bogus group.
The analogous skill case: with
~/.agentsunder git, every CLI-installed skill shows the~/.agentsremote instead of its real source from~/.agents/.skill-lock.json.Root fix
Tool-managed extensions should take their source from the tool's own install manifest, falling back to
detect_sourceonly for genuinely git-cloned extensions with no manifest entry:<root>/.skill-lock.json(theskillsCLI lockfile), keyed by skill name:source(owner/repo) +sourceUrl.~/.claude/plugins/installed_plugins.json(<plugin>@<marketplace>) +~/.claude/plugins/known_marketplaces.json(marketplace →owner/repo).This subsumes the symptom-level patches: with the authoritative source in hand there is no polluted
source.url/install_metato outrank, and the symlink canonicalize/heal in #88 become defensive rather than load-bearing.Scope of the proposed PR
scanner::scan_skill_dir: afterdetect_source, override with the lockfile entry when present (cached per lockfile, one read per scanned dir).PluginEntry: newsource_urlfield; Claude's adapter fills it fromknown_marketplaces.json;scanner::scan_pluginsprefers it over.gitdetection. Other adapters leave itNone(unchanged behavior).Deferred / follow-up
source_urlresolution later (keptNonehere to stay surgical).install_metafrom the inferred enclosing.git) can be narrowed or retired.Depends on #88 (built on its
canonicalizeinscan_skill_dir). PR incoming.