Skip to content

Instantly share code, notes, and snippets.

@smileart
Created May 28, 2026 10:00
Show Gist options
  • Select an option

  • Save smileart/b94dbf685e29aea8fff78a481d757775 to your computer and use it in GitHub Desktop.

Select an option

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
#!/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