diff --git a/packages/mosaic/framework/tools/_scripts/mosaic-sync-skills b/packages/mosaic/framework/tools/_scripts/mosaic-sync-skills index 0a9d02f9..e57aad17 100755 --- a/packages/mosaic/framework/tools/_scripts/mosaic-sync-skills +++ b/packages/mosaic/framework/tools/_scripts/mosaic-sync-skills @@ -262,7 +262,12 @@ prune_stale_links_in_target() { # -m resolves lexical dangling targets too. If resolution fails, ownership # is unproven and the link must be preserved. resolved="$(readlink -m "$link_path" 2>/dev/null || true)" - if [[ -n "$resolved" && "$resolved" == "$canonical_real/"* ]]; then + # $canonical_real must be length-checked BEFORE use as a prefix: if it were + # ever empty, "$resolved" == "$canonical_real/"* collapses to == "/"* and + # matches every absolute path. Combined with the is_mosaic_skill_name skip + # above, that inverts the function precisely — it would delete exactly the + # FOREIGN symlinks and keep the mosaic ones. (#1087, reported by mos-claude.) + if [[ -n "$resolved" && -n "$canonical_real" && "$resolved" == "$canonical_real/"* ]]; then rm -f "$link_path" echo "[mosaic-skills] Removed stale retired skill link: $link_path" fi