--- title: Install / Build Cross-Platform Parity (PS + Bash) type: concept updated: 2026-05-25 --- # Install / Build Cross-Platform Parity (PS + Bash) Sibling concept to `install-portability.md` (POSIX-shell compat). This one is about the **paired-script parity contract** between `scripts/install.ps1` / `scripts/install.sh` (install) and `scripts/build.ps1` / `scripts/build.sh` (build). Both pairs share the same conventions and the same `--prune` / `-Prune` flag pattern. ## Why two scripts Windows hosts running CC outside a git-bash terminal have no reliable bash. Native PowerShell call (`pwsh ./scripts/install.ps1`) is the friction-free path. Linux / macOS users get bash. Both audiences are first-class — `claude-skills` is multi-machine by design (see `project_deployment_goal` memory). Single-script "use bash everywhere" was rejected: forces every Windows user to install git-bash before bootstrap, contradicts "tool-light install path". ## Parity contract Both scripts MUST: - Read sources from `/skills//`. - Install to `$CLAUDE_SKILLS_DIR` if set, else `~/.claude/skills/`. - Accept a name-list (positional in bash, `-Names` in PS); no args = all. - Skip with a warning when `skills//` is missing or has no `SKILL.md`. - Idempotent: `rm -rf` (or `Remove-Item -Recurse -Force`) the destination, then copy fresh. - Print one `installed: -> ` line per successfully installed skill. Flag naming follows the host shell's convention — POSIX `--prune` in bash, PascalCase `-Prune` switch in PowerShell. Behaviour identical. ## The `--prune` / `-Prune` flag Added 2026-05-25 (commit `6cf0e98`). Motivating case: after retiring `using-synology-ops` the installed dir `~/.claude/skills/using-synology-ops/` lingered until manual `rm` — the install scripts had no notion of stale-cleanup. **Behaviour:** after the install loop, walk `$target/*` and remove any dir whose name is not in `skills/*`. **Design choices and their rejected alternatives:** - **Combined flag, not standalone mode.** Single invocation does both. Alternative (`install --prune-only`) was rejected — adds a mode that nobody asked for; users wanting "just cleanup" can pass an empty name list (`install.sh --prune` with no positional args still walks the prune step at the end, no-op'ing the install loop because all source skills resolve to no-op overwrites of fresh installs). - **Global scan, ignores name filter.** Even when called as `install.sh foo --prune`, the prune step scans the full target against the full source. Rationale: stale-cleanup is a global concern. A user who explicitly opts into prune wants the cleanup to be useful — a names-filtered prune ("only prune dirs that match the name list AND are missing from source") rarely matches anyone's mental model. - **Print-and-delete, no confirmation prompt.** Each removal prints `pruning: (not in skills/) -> `. Confirmation prompts would block automation (CI, batch reinstalls). Visibility comes from the printed line; users wanting a dry-run pass `-WhatIf` in PowerShell (the cmdlet already supports it) or pipe to `echo` in bash (trivial to grep `^pruning:` before running for real). - **Default off.** Must be passed explicitly. Idempotent re-installs (the common case) don't suddenly delete anything. ## What install-side `--prune` does NOT do - It does not touch plugin-installed skills under `~/.claude/plugins//skills//`. Those are managed by the plugin system, not this repo. - It does not warn if the about-to-be-deleted dir contains user-edited content. The contract is that `~/.claude/skills//` is a managed copy of `skills//` — anything else is user-error. - It does not remove `dist/.skill` build artefacts. That's the build-script's `--prune` (see next section). ## Build-side `--prune` / `-Prune` Added 2026-05-25 — natural extension of the install-side flag to the build pair (`scripts/build.sh` and `scripts/build.ps1`). Same design choices, applied to files instead of directories: after the build loop, walk `dist/*.skill` and remove any whose `` (basename minus `.skill`) is not in `skills/*`. Identical to install-side: combined flag, global scan ignores the name filter, prints `pruning: -> ` per removal, default off, no confirmation prompt. The bash path has one extra wrinkle: when `build.sh` is run on Windows without `zip` and delegates to `build.ps1` via `powershell.exe -File`, the `--prune` flag is **not** forwarded to the delegated PS process. Bash runs the prune step itself at the end of the script, against the same `dist/` directory. This keeps the delegation surface narrow (no flag-translation bugs) and the prune logic single-sourced per shell. `build.ps1` invoked directly (without the bash wrapper) handles `-Prune` natively. ## What build-side `-Prune` does NOT do - It does not unblock build for skills that have been renamed mid-flight. A user who renamed `skills/foo/` → `skills/bar/` should still run a fresh build (`build.sh bar`) — prune only catches stale archives whose source dir is gone, not stale archives whose source was renamed (those become orphans of a different source, indistinguishable from intentional foreign artefacts). - It does not touch `dist-hermes/` — that directory is managed by `scripts/build-hermes.py` and follows its own rules (whole-directory rebuild per skill). Hermes has no current prune mechanism; if needed, that's a separate concept page. ## Test evidence All four scripts smoke-tested 2026-05-25. **Install side** — disposable target dirs (env-overridden `CLAUDE_SKILLS_DIR`). Pre-populated with 2 fake stale dirs, ran full install + prune, verified: stale dirs removed, all real skills installed, retired `using-synology-ops` absent. **Build side** — fake `dist/fake-stale.skill` + `dist/another-stale.skill` files created directly in the real `dist/`. Ran `build.sh --prune fake-stale-sh` (positional arg triggers a no-op build via skip path; prune runs at the end) and `build.ps1 -Names fake-stale-ps -Prune`. Both removed the fakes, left real archives like `caveman.skill` untouched. No automated test fixture in the repo — install / build scripts are wrapper-style, smoke-test evidence in the respective commit bodies suffices under the `[skip-tdd: wrapper]` carve-out. `[archive-roundtrip-test]` (still ⚪ on the board) is a candidate place to add a real fixture once it lands.