Stock macOS ships bash 3.2 (frozen 2007 due to GPLv3) and BSD find. Both scripts used `mapfile` (bash 4+) and `find -printf` (GNU only), so a fresh Mac user running `bash scripts/install.sh` died on line 11 before copying anything. Replace with a portable shell glob — same sort order, no `find` dependency, works on bash 3.2 and 4+. Verified on git-bash: 16 skills discovered, install dry-run copies all, `build.sh` produces a valid archive. See .wiki/concepts/install-portability.md for the full gotcha. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
3.0 KiB
title, type, updated
| title | type | updated |
|---|---|---|
| Install / Build Script Portability | concept | 2026-04-28 |
Install / Build Script Portability
scripts/install.sh and scripts/build.sh must run on three combos:
- Windows + git-bash (primary dev workstation)
- Linux + bash (typical server)
- macOS + bash (the gotcha)
Stock macOS ships bash 3.2 (frozen at 2007 because GPLv3) and BSD find. Both diverge from what Linux's bash 4+ / GNU find accept.
What broke before the fix
Both scripts originally used:
mapfile -t names < <(find "$SRC" -mindepth 1 -maxdepth 1 -type d -printf '%f\n' | sort)
Two failures on stock Mac:
mapfile— bash 4+ builtin. Bash 3.2 throwsmapfile: command not found.find -printf— GNU extension. BSD find throwsfind: -printf: unknown primary or operatorand exits non-zero, killing the pipeline underset -euo pipefail.
Result: bash scripts/install.sh died on line 11 with a confusing error, before copying anything. Same for build.sh.
Fix
Replace with shell glob — pure POSIX-compatible bash 3.2 syntax, no find flavor dependency:
names=()
for d in "$SRC"/*/; do
[ -d "$d" ] || continue
names+=("$(basename "$d")")
done
IFS=$'\n' names=($(printf '%s\n' "${names[@]}" | sort))
unset IFS
for d in dir/*/— glob expansion. Trailing slash filters to directories.[ -d "$d" ] || continue— guards the empty-dir case (when no subdirs exist, the literal patterndir/*/is left unexpanded by default; the test rejects it).basename— POSIX, present everywhere.IFS=$'\n' ... sort— preserves alphabetical order to match the previous behavior (find | sort).
$'...' ANSI-C quoting is bash 2+. The whole approach works identically on bash 3.2 (Mac), bash 4 (Linux), bash 5 (modern Linux + brew on Mac), and the bash bundled with git-for-Windows.
What still requires bash, not POSIX sh
The shebangs are still #!/usr/bin/env bash. Arrays (names=(), names+=(...), "${names[@]}") are bash, not POSIX. That's fine — bash is on every target OS. Demoting to sh would force a much uglier rewrite for no real portability gain.
What was already portable
cp -R "$src" "$dst"— POSIX flag, works everywhere.rm -rf— POSIX.mkdir -p— POSIX.~/.claude/skills/—~expands the same on all three shells.- The
zip/PowerShell branching inbuild.shalready handled "no zip on stock Windows" — the Mac path simply useszip, which ships in/usr/bin/zipon every macOS release since forever.
Test coverage
Manually verified on Windows + git-bash after the fix: 16 skills discovered, sorted, installed to $CLAUDE_SKILLS_DIR. build.sh active-platform produced a valid archive. No real BSD-find / bash-3.2 environment was tested — confidence comes from the fact that the new code uses only POSIX-shell + bash-2 features, all explicitly supported on stock macOS.
The archive-roundtrip-test task on the board would catch any regression here automatically.