github runkids/skillshare v0.16.7

latest releases: v0.19.5, v0.19.4, v0.19.3...
one month ago

πŸ”— skillshare v0.16.7 Release Notes

Release date: 2026-03-02

TL;DR

v0.16.7 is a dotfiles manager compatibility release:

  1. External symlinks preserved β€” sync no longer breaks target symlinks created by stow, chezmoi, yadm, etc.
  2. Symlinked source directories across all commands β€” ~/.config/skillshare/skills/ can be a symlink (even chained) and all commands (sync, update, uninstall, list, status, collect, install, diff) discover skills correctly
  3. Status/collect aware β€” status and collect follow external target symlinks instead of reporting conflicts or skipping them
  4. Group containment guard β€” uninstall --group and update --group reject groups that symlink outside the source tree

The Problem

Dotfiles managers (GNU Stow, chezmoi, yadm, bare-git) commonly manage AI tool config directories through symlinks:

~/.claude/skills/ β†’ ~/dotfiles/claude-skills/     # stow-managed
~/.config/skillshare/skills/ β†’ ~/dotfiles/ss/     # source also symlinked

Before v0.16.7, skillshare would:

  • Delete the ~/.claude/skills/ symlink when converting from symlinkβ†’merge mode (breaking the dotfiles manager setup)
  • Fail to discover skills when the source directory was a symlink (affected update, uninstall, reconcile, and server handlers β€” not just sync)
  • Report "conflict" for targets that were external symlinks
  • Skip scanning external target symlinks in collect

Root cause: filepath.Walk vs symlinked roots

filepath.Walk and filepath.WalkDir use os.Lstat on the root, which does not follow symlinks. If root is a symlink, info.IsDir() returns false and the walk callback never enters it. This affected 10+ callsites beyond sync:

  • internal/config/reconcile.go β€” ReconcileGlobalSkills
  • internal/config/project_reconcile.go β€” ReconcileProjectSkills
  • internal/install/install_queries.go β€” getUpdatableSkillsImpl, FindRepoInstalls, getTrackedReposImpl
  • cmd/skillshare/update.go β€” cmdUpdate --all
  • cmd/skillshare/update_resolve.go β€” resolveGroupUpdatable
  • cmd/skillshare/update_project.go β€” updateAllProjectSkills
  • cmd/skillshare/uninstall.go β€” resolveGroupSkills, resolveNestedSkillDir, countGroupSkills
  • internal/server/handler_update.go β€” getServerUpdatableSkills

os.ReadDir does follow symlinked roots (uses os.Open), so callsites using ReadDir (doctor, audit) were unaffected.

Symlink Sync Decision Flow

The core fix for target symlinks is isSymlinkToSource() β€” before removing a target symlink, sync checks whether it points to the skillshare source directory:

Target is a symlink?
β”œβ”€β”€ YES β†’ Points to source directory?
β”‚   β”œβ”€β”€ YES β†’ Skillshare's own symlink-mode link
β”‚   β”‚         β†’ Remove it (converting to merge/copy mode)
β”‚   └── NO  β†’ External symlink (dotfiles manager, etc.)
β”‚             β†’ Preserve it, sync INTO the resolved directory
└── NO  β†’ Regular directory
          β†’ Sync normally (create skill symlinks inside)

Source Directory Resolution

utils.ResolveSymlink() (extracted from sync.go's local resolveWalkRoot()) calls filepath.EvalSymlinks() on the path before walking:

Source: ~/.config/skillshare/skills/  (symlink)
  β†’ ~/dotfiles/ss/                   (resolved)
    β†’ Walk resolved path for SKILL.md files
    β†’ Compute RelPath relative to resolved root
    β†’ Store SourcePath using original symlink path (for display)

This also handles chained symlinks: link2 β†’ link1 β†’ real_dir.

Group Operation Containment Guard

uninstall --group and update --group now verify that the resolved group path stays within the source tree:

Group dir is a symlink?
β”œβ”€β”€ Resolved path inside source? β†’ Proceed normally
└── Resolved path outside source? β†’ Reject with error:
    "group 'evil-group' resolves outside source directory"

This prevents a crafted symlink (e.g., skills/evil-group β†’ /important/data) from causing unintended deletions or updates outside the source tree.

What Changed

File Change
internal/utils/path.go New ResolveSymlink() shared utility
internal/sync/sync.go Replaced local resolveWalkRoot() with utils.ResolveSymlink()
internal/sync/copy.go SyncTargetCopyWithSkills checks isSymlinkToSource() before removing
internal/sync/pull.go FindLocalSkills follows external target symlinks
internal/config/reconcile.go Resolve source before WalkDir
internal/config/project_reconcile.go Resolve source before WalkDir
internal/install/install_queries.go Resolve source in 3 walk functions
cmd/skillshare/update.go Resolve cfg.Source before Walk
cmd/skillshare/update_resolve.go Resolve + containment guard
cmd/skillshare/update_project.go Resolve uc.sourcePath before Walk
cmd/skillshare/uninstall.go Resolve + containment guard in 3 functions
internal/server/handler_update.go Resolve source before WalkDir
cmd/skillshare/upgrade.go Clear prompt lines to preserve tree layout

Testing

  • Unit tests: internal/sync/symlinked_dir_test.go (560 lines) β€” covers symlinked source, symlinked target, double symlink, chained symlinks, external symlink preservation, copy mode, merge mode
  • Integration tests: tests/integration/sync_symlinked_dir_test.go (378 lines) β€” end-to-end CLI tests with testutil.Sandbox, including containment guard rejection tests
  • E2E runbook: ai_docs/tests/symlinked_dir_sync_runbook.md β€” 20-step manual validation for devcontainer covering sync, update, uninstall, collect, reconcile, and containment guard scenarios

Upgrading

# Homebrew
brew upgrade skillshare

# Direct download
skillshare upgrade

# Or download from GitHub Releases
# https://github.com/runkids/skillshare/releases/tag/v0.16.7

Changelog

  • b8b48aa fix(symlink): resolve symlinked source/target dirs across all Walk callsites
  • abe64cc fix(sync): preserve external symlinks during sync (dotfiles manager support)
  • 70a8e79 fix(sync): preserve external symlinks in merge/copy mode conversions
  • 8519ebe fix(upgrade): clear prompt lines to preserve tree layout

Don't miss a new skillshare release

NewReleases is sending notifications on new releases.