Created
May 28, 2026 10:00
-
-
Save smileart/b94dbf685e29aea8fff78a481d757775 to your computer and use it in GitHub Desktop.
Matt Pocock skills installer for Stow-like .file management tools where .claude is a symlink
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env bash | |
| # | |
| # setup-mattpocock-skills.sh | |
| # | |
| # Installs (or refreshes) mattpocock/skills and works around the installer's | |
| # broken-symlink behaviour. See upstream bugs: | |
| # https://github.com/mattpocock/skills/issues/216 | |
| # https://github.com/mattpocock/skills/issues/141 | |
| # | |
| # The installer puts skills into ~/.agents/skills/ and writes symlinks into | |
| # ~/.claude/skills/ using relative paths like `../../.agents/skills/<name>`. | |
| # When ~/.claude/skills is itself a symlink (e.g. it lives inside a dotfiles | |
| # repo), the relative target fails to resolve through the symlink chain and | |
| # Claude Code reports the skill as missing. | |
| # | |
| # This script: | |
| # 1. Runs the installer (bunx by default, npx with --npx). | |
| # 2. Scans ~/.claude/skills for relative symlinks and rewrites them to | |
| # absolute paths. Idempotent; safe to re-run. | |
| # | |
| # Usage: | |
| # ./setup-mattpocock-skills.sh # bunx install + fix | |
| # ./setup-mattpocock-skills.sh --npx # npx install + fix | |
| # ./setup-mattpocock-skills.sh --pm pnpm # custom runner (pnpm dlx) | |
| # ./setup-mattpocock-skills.sh --no-install # just fix existing symlinks | |
| # ./setup-mattpocock-skills.sh --dry-run # preview fixes, no changes | |
| # ./setup-mattpocock-skills.sh --help | |
| # | |
| # Environment: | |
| # SKILLS_DIR Override target dir (default: ~/.claude/skills). | |
| # SKILLS_PKG Override package spec (default: skills@latest). | |
| # SKILLS_REPO Override repo arg (default: mattpocock/skills). | |
| set -euo pipefail | |
| SKILLS_DIR="${SKILLS_DIR:-$HOME/.claude/skills}" | |
| SKILLS_PKG="${SKILLS_PKG:-skills@latest}" | |
| SKILLS_REPO="${SKILLS_REPO:-mattpocock/skills}" | |
| PM="bunx" | |
| DO_INSTALL=1 | |
| DRY_RUN=0 | |
| while (( $# )); do | |
| case "$1" in | |
| --bunx) PM="bunx" ;; | |
| --npx) PM="npx" ;; | |
| --pm) shift; PM="${1:-}"; [[ -n "$PM" ]] || { echo "error: --pm needs a value" >&2; exit 2; } ;; | |
| --no-install) DO_INSTALL=0 ;; | |
| -n|--dry-run) DRY_RUN=1; DO_INSTALL=0 ;; | |
| -h|--help) | |
| grep '^#' "$0" | sed 's/^# \{0,1\}//' | |
| exit 0 | |
| ;; | |
| *) | |
| echo "unknown arg: $1 (try --help)" >&2 | |
| exit 2 | |
| ;; | |
| esac | |
| shift | |
| done | |
| run_installer() { | |
| if ! command -v "$PM" >/dev/null 2>&1; then | |
| echo "error: package runner '$PM' not found in PATH" >&2 | |
| exit 1 | |
| fi | |
| # bunx / npx use the same arg shape: <runner> <pkg> <subcommand> <args...> | |
| # pnpm and yarn need an explicit dlx subcommand. | |
| case "$PM" in | |
| pnpm) set -- "$PM" dlx "$SKILLS_PKG" add "$SKILLS_REPO" ;; | |
| yarn) set -- "$PM" dlx "$SKILLS_PKG" add "$SKILLS_REPO" ;; | |
| *) set -- "$PM" "$SKILLS_PKG" add "$SKILLS_REPO" ;; | |
| esac | |
| echo "==> running: $*" | |
| "$@" | |
| } | |
| normalize_path() { | |
| awk -F/ '{ | |
| n = 0 | |
| for (i = 1; i <= NF; i++) { | |
| if ($i == "" || $i == ".") continue | |
| if ($i == "..") { if (n > 0) n--; continue } | |
| stack[++n] = $i | |
| } | |
| out = "" | |
| for (i = 1; i <= n; i++) out = out "/" stack[i] | |
| print (out == "" ? "/" : out) | |
| }' | |
| } | |
| fix_symlinks() { | |
| if [[ ! -d "$SKILLS_DIR" ]]; then | |
| echo "error: $SKILLS_DIR does not exist" >&2 | |
| exit 1 | |
| fi | |
| # Logical (NOT physical) absolute path. Following the parent symlink would | |
| # canonicalise into the wrong base dir for resolving relative targets — | |
| # which is exactly the bug we're working around. | |
| local abs_skills_dir | |
| case "$SKILLS_DIR" in | |
| /*) abs_skills_dir="$SKILLS_DIR" ;; | |
| "~"|"~/"*) abs_skills_dir="${SKILLS_DIR/#~/$HOME}" ;; | |
| *) abs_skills_dir="$PWD/$SKILLS_DIR" ;; | |
| esac | |
| abs_skills_dir="$(printf '%s' "$abs_skills_dir" | normalize_path)" | |
| local fixed=0 skipped=0 missing=0 | |
| shopt -s nullglob | |
| for entry in "$SKILLS_DIR"/*; do | |
| [[ -L "$entry" ]] || continue | |
| local name target abs_target | |
| name="$(basename "$entry")" | |
| target="$(readlink "$entry")" | |
| if [[ "$target" == /* ]]; then | |
| skipped=$((skipped + 1)) | |
| continue | |
| fi | |
| abs_target="$(printf '%s/%s' "$abs_skills_dir" "$target" | normalize_path)" | |
| if [[ ! -e "$abs_target" ]]; then | |
| echo "missing: $name -> $abs_target (skipped, target does not exist)" | |
| missing=$((missing + 1)) | |
| continue | |
| fi | |
| if (( DRY_RUN )); then | |
| echo "would fix: $name : $target => $abs_target" | |
| else | |
| rm "$entry" | |
| ln -s "$abs_target" "$entry" | |
| echo "fixed: $name -> $abs_target" | |
| fi | |
| fixed=$((fixed + 1)) | |
| done | |
| local verb="fixed" | |
| (( DRY_RUN )) && verb="would fix" | |
| echo | |
| echo "summary: $verb $fixed, skipped $skipped already-absolute, $missing missing" | |
| } | |
| (( DO_INSTALL )) && run_installer | |
| fix_symlinks |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment