π skillshare v0.16.7 Release Notes
Release date: 2026-03-02
TL;DR
v0.16.7 is a dotfiles manager compatibility release:
- External symlinks preserved β sync no longer breaks target symlinks created by stow, chezmoi, yadm, etc.
- 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 - Status/collect aware β
statusandcollectfollow external target symlinks instead of reporting conflicts or skipping them - Group containment guard β
uninstall --groupandupdate --groupreject 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 justsync) - 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βReconcileGlobalSkillsinternal/config/project_reconcile.goβReconcileProjectSkillsinternal/install/install_queries.goβgetUpdatableSkillsImpl,FindRepoInstalls,getTrackedReposImplcmd/skillshare/update.goβcmdUpdate --allcmd/skillshare/update_resolve.goβresolveGroupUpdatablecmd/skillshare/update_project.goβupdateAllProjectSkillscmd/skillshare/uninstall.goβresolveGroupSkills,resolveNestedSkillDir,countGroupSkillsinternal/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 withtestutil.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.7Changelog
- 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