Skip to content

Symlinked skills mis-attributed to enclosing agent-home git repo, splitting one skill into two rowsΒ #87

Description

@SherlockSalvatore

Symlinked skills are mis-attributed to the enclosing agent-home git repo, splitting one skill into two rows

Summary

When an agent home (e.g. ~/.claude) is kept inside the user's own dotfiles git repo, a skill that was installed into the shared ~/.agents/skills/<name> location and symlinked into ~/.claude/skills/<name> is mis-attributed: the scanner walks the symlink's textual parents, hits ~/.claude/.git, and records the dotfiles repo as the skill's source. The git-source backfill in store.rs then stamps that copy with install_type = 'git' + the dotfiles install_url/pack.

Result: the single on-disk skill (one path is literally a symlink to the other) shows up as two rows in Extensions β€” one correctly sourced from the marketplace repo (the agents that scan ~/.agents/skills directly), and one falsely sourced from the user's dotfiles repo (Claude, via the symlink).

Reproduce

  1. Keep ~/.claude under a personal dotfiles git repo (a common backup setup).
  2. Install a multi-agent skill that uses the shared layout, e.g. npx skills@latest add mattpocock/skills. This creates the canonical copy at ~/.agents/skills/tdd and a symlink ~/.claude/skills/tdd -> ../../.agents/skills/tdd.
  3. Open Extensions and filter for the skill.

Expected: one row, all agents, source = mattpocock/skills, updatable together.
Actual: two rows β€” mattpocock/skills (Codex/Gemini/Copilot) and <you>/<dotfiles-repo> (Claude, install_type=git).

Live DB confirms (~/.harnesskit/metadata.db):

agent source.url pack install_url install_type
claude <dotfiles> <you>/<dotfiles> <dotfiles> git
codex/gemini/copilot null mattpocock/skills mattpocock/skills marketplace

Root cause

  • scanner::detect_source walks textual parent dirs for .git. For a symlinked skill entry it finds the repo the link sits under, not the one the content lives in.
  • store.rs sync_extensions / sync_extensions_for_agent then backfill install_type='git' from that polluted source_json.
  • Grouping keys on the derived developer (install_meta.url β†’ source.url β†’ pack), so the two copies fall into different groups.

This also defeats #76 (prefer install_meta.url for grouping): here install_meta itself is the wrong dotfiles URL, so preferring it still yields the wrong source.

Proposed fix

  1. Scanner: resolve symlinks (canonicalize) before walking up for .git, so a symlinked skill is attributed to its real content's source. Plain skills canonicalize to the same tree β†’ unchanged.
  2. Store (self-heal existing DBs): clear the bogus git install_meta (and the pack derived from it) for skill rows whose on-disk entry is a symlink and whose freshly-scanned source is non-git. Real git installs are plain files (never symlinks), so they are untouched.

After both, the symlinked Claude copy scans as sourceless and re-attaches to its marketplace sibling via the existing urlSiblings grouping pass β†’ one row, updatable together.

PR incoming.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions