Skip to content
camundaPublic

About

A command line tool that helps you manage external skills in your project as dependencies

Resources

Code of conduct

Security policy

Stars

8 stars

Watchers

0 watching

Forks

Repository files navigation

spm — skill package manager

CI crates.io npm License

Declare AI skills as git dependencies in ai.json, and spm wires them into your AI tool (Amp, Claude Code, Cline, OpenAI Codex CLI, GitHub Copilot CLI, Cursor, Gemini CLI and Windsurf) without ever committing skills to your repo. Anything spm materializes into the working tree is gitignored — no symlinks, no skills under version control.

🌐 Manage skills at project scope (the default — tracked in ai.json, shared with your team) or user-global scope (-g/--global on the scope-aware commands — one skill set available to every project on your machine). See Global skills.

📖 Documentation: https://camunda.github.io/spm-cli/ (built from docs/ and deployed via GitHub Pages).

How it works

ai.json ──resolve──▶ ai.lock ──fetch──▶ ~/.spm/store/<repo>@<sha>   (global cache, one clone per commit)
                                              │
                                              └─project──▶ materialized where the vendor expects it (see below)
  • ai.json — you author it, commit it. Declares target vendors + skill deps.
  • ai.lock — generated, commit it. Pins every version selector to an immutable commit SHA → reproducible installs.
  • Global store (~/.spm/store) — a fetch cache only: each repo@commit is cloned once and shared across all projects. Nothing is registered or materialized here — it exists purely so repeated installs don't re-clone.
  • Vendor projection — spm copies the store's skills into the directory where each vendor loads them from. In the default project scope that is a project-local, gitignored dir — nothing spm generates is committed. With -g/--global (see Global skills) it materializes into a user-global location shared across all your projects instead.
  • Registration differs per vendor:
    • Claude — spm assembles a self-contained plugin marketplace in the project-local, gitignored .spm/claude/ dir and writes a pointer to it into .claude/settings.local.json (gitignored by convention). The dir sits outside .agents/skills/ so Copilot's scanner never picks it up. Declarative, per-project, zero VCS footprint.
    • Copilot CLI — spm copies the resolved skills into a project-local directory, .agents/skills/spm-managed-skills/<name>/, where Copilot CLI auto-discovers them (.agents/skills/**/SKILL.md). That directory is added to the project's .gitignore (with an explanatory comment) so the materialized skills stay truly local and are never committed. No user-global state, no copilot CLI required.
    • Gemini CLI — spm copies the resolved skills one directory deep into the tool-native .gemini/skills/<name>/, where Gemini CLI auto-discovers them. Because Gemini treats that dir as a team-shared, version-controlled location, spm shares it with your own hand-authored skills: it never wipes the dir, touches only the entries it manages, and gitignores just those spm-managed subdirs (.gemini/skills/<name>/) so they stay local while your own skills remain committable.
    • Codex CLI — spm copies the resolved skills one directory deep into the cross-tool .agents/skills/<name>/ alias (the same standard location Copilot and Gemini can read), where Codex CLI auto-discovers them. Same shared-dir handling as Gemini: spm never wipes the dir, touches only its managed entries, and gitignores just those spm-managed subdirs (.agents/skills/<name>/).
    • Cursor — spm copies the resolved skills one directory deep into the tool-native .cursor/skills/<name>/, where Cursor auto-discovers them. Same shared-dir handling as Gemini: Cursor treats that dir as version-controlled, so spm never wipes it, touches only the entries it manages, and gitignores just those spm-managed subdirs (.cursor/skills/<name>/).
    • Cline — spm copies the resolved skills one directory deep into the tool-native .cline/skills/<name>/, where Cline auto-discovers them. Same shared-dir handling as Gemini (never wipes the dir, surgical per-skill add/remove, per-skill gitignore).
    • Windsurf — spm copies the resolved skills one directory deep into the tool-native .windsurf/skills/<name>/, where Windsurf's Cascade agent auto-discovers them. Same shared-dir handling as Gemini. (Note: Windsurf's global skills live under ~/.codeium/windsurf/skills/, not ~/.windsurf/.)
    • Amp — spm copies the resolved skills one directory deep into the cross-tool .agents/skills/<name>/ alias (Amp's documented default, the same dir Codex reads), where Amp auto-discovers them. Same shared-dir handling as Gemini.

On a fresh clone, teammates run spm install — it repopulates their own fetch cache and re-materializes the project-local skills from ai.lock. Same model as node_modules.

In CI, run spm install --locked (like npm ci / cargo build --locked): it installs exactly the commits pinned in ai.lock and fails instead of re-resolving when ai.lock is out of sync with ai.json — a dependency missing from the lock, one whose git/selector/path changed, a stale lock entry no longer declared, or (with resolveTransitive) a transitive child without a pin. Every mismatch is reported in one error, and ai.lock is never written. It still fetches into the store; it is not an offline mode.

ai.json

{
  "targets": ["claude", "copilot"],
  "skills": {
    "pdf-tools": { "git": "https://github.com/org/skills", "tag": "v1.2.0", "path": "skills/pdf" },
    "reviewer":  { "git": "https://github.com/me/reviewer", "branch": "main" },
    "linter":    { "git": "https://github.com/org/linter", "version": "^1.2" },
    "pinned":    { "git": "https://github.com/x/y",         "commit": "a1b2c3d" }
  }
}

targets lists one or more vendors (amp, claude, cline, codex, copilot, cursor, gemini, windsurf) — skills resolve once and project into each independently.

Schema & validation

ai.json is described by a JSON Schema at schema/ai.schema.json (draft-07). spm embeds it and validates every ai.json on load, reporting all violations at once with their JSON path:

error: in ai.json: ai.json does not match schema:
  at /skills/x: {"git":"u"} is not valid under any of the schemas listed in the 'oneOf' keyword

Add a "$schema" reference for editor autocompletion/validation:

{ "$schema": "./schema/ai.schema.json", "targets": ["claude"], "skills": {} }

Version selectors (exactly one per skill):

field meaning locked to
tag git tag (annotated tags deref to commit) resolved SHA
branch branch tip at install/update time resolved SHA
commit exact commit itself
version highest semver tag matching a range (^1.2) resolved tag + SHA

version takes a semver requirement (^1.2, ~1.4, >=1.2, <2, 1) and locks the highest tag that satisfies it. Tags may carry one leading v (v1.4.1 and 1.4.1 both count); non-semver tags (latest, v1) are ignored and pre-releases only match a requirement that names one (~1.5.0-rc.1). ai.lock records the requirement, the tag it chose and its commit; spm install keeps that pin, and spm update moves it to the newest tag still inside the range — never past it (a ^1.2 skill never jumps to v2.0.0). An exact tag is never treated as a range.

path (optional) selects a subdirectory — for monorepos holding many skills.

spm add with no selector uses the remote's default branch and writes it to ai.json as an explicit branch (the chosen branch is printed). A hand-edited entry must still set exactly one selector.

Transitive dependencies (resolveTransitive)

By default spm only resolves the skills you list. Set the top-level "resolveTransitive": true and each resolved skill's own co-located ai.json is read recursively, pulling in the skills it declares alongside yours (deduplicated, cycle-safe, with a depth cap):

{ "targets": ["claude"], "resolveTransitive": true, "skills": { "toolkit": { "git": "https://github.com/org/toolkit", "tag": "v2.0.0" } } }

It is opt-in because skills can carry executable agent instructions, so auto-fetching repos you never named is a larger supply-chain surface. The flag is read only from your root manifest — a dependency can't re-enable it — and every fetched skill still passes the content scan before its nested manifest is read. Two skills requiring the same repo at different commits is a hard error. See the Transitive Skill Dependencies guide for naming, provenance (spm list/status), and conflict resolution.

Source allowlist (allowedSources)

Restrict where skills and plugins may come from with an allowlist of host/path patterns (no scheme; * as the whole last segment matches anything below, file://* allows local sources):

{ "targets": ["claude"], "allowedSources": ["github.com/camunda/*", "github.com/me/one-skill"], "skills": {} }

Every source — direct, transitive, re-installed from ai.lock or the store cache — must match before spm contacts it; add fails without touching ai.json, and --protocol/--protocol-fallback can't route around it. HTTPS, ssh:// and scp-style spellings of one repo match the same pattern. A machine policy file (SPM_POLICY, else $SPM_HOME/policy.json, shape { "allowedSources": [...] }) is intersected with the project list, so a project can narrow but never widen it; a dependency's nested ai.json may not declare allowedSources. See the Source Policy guide.

Full plugins (plugins)

Alongside individual skills, ai.json can depend on a full Claude Code plugin — one that bundles agents, MCP servers, hooks and scripts in addition to (or instead of) skills. Declare it under a plugins map, keyed by local name, with the same git/version selectors as a skill; path points at the plugin root (the directory holding .claude-plugin/plugin.json):

{
  "targets": ["claude", "copilot"],
  "plugins": {
    "design-system": {
      "git": "https://github.com/camunda/design-system",
      "branch": "main",
      "path": "plugins/camunda-design-system"
    }
  }
}

On install:

  • Claude gets the whole plugin registered under a dedicated, project-local spm-plugins marketplace (.spm/claude-plugins/), so its agents, MCP servers, hooks and scripts all load — not just its SKILL.md.
  • Every target (including skills-only ones like Copilot, Gemini, …) still gets the plugin's bundled skills, flattened into that target's normal skills dir — a graceful, skills-only degradation.
  • ai.lock pins the plugin's commit and records its bundled skill set.

A bundled skill whose name collides with a standalone skills entry (or another plugin's skill) is a hard error, never a silent overwrite.

Add or remove a plugin from the CLI with the --plugin flag (point --path at the plugin root):

spm add https://github.com/camunda/design-system --branch main \
        --path plugins/camunda-design-system --plugin --name design-system
spm remove design-system --plugin

To pull in every skill under a directory at once (each immediate subdirectory that has its own SKILL.md), add --all instead of naming them one by one:

spm add https://github.com/org/repo --tag v1.0.0 --path skills --all

Each sub-skill becomes its own ai.json entry, keyed by its directory name (--all cannot be combined with --name). This is the one-shot equivalent of the per-skill spm add … --path <sub> --name <sub> commands spm suggests when you point --path at a container of skills.

Repo URLs (HTTPS & SSH)

git accepts any URL the system git understands:

spm add https://github.com/org/repo --tag v1.0.0            # HTTPS
spm add git@github.com:org/repo.git --branch main           # SSH (scp-style)
spm add ssh://git@github.com/org/repo.git --branch main     # SSH (url form)

For GitHub there is a shorthand: github.com/<owner>/<repo>[@<ref>] expands to https://github.com/<owner>/<repo>.git, and @<ref> replaces the version flag. spm looks the ref up on the remote and treats it as a tag or a branch (a full 40-character SHA is taken as a commit without a lookup). It is an error if the ref is neither, or is both a tag and a branch (use the explicit flag), or if @<ref> is combined with --tag, --branch, --commit or --version. ai.json always records the expanded URL, never the shorthand. Anything not starting with github.com/ is passed through untouched.

spm add github.com/org/repo@v1.0.0        # = spm add https://github.com/org/repo.git --tag v1.0.0
spm add github.com/org/repo@main          # = ... --branch main
spm add github.com/org/repo --tag v1.0.0  # shorthand URL, explicit flag
spm add github.com/org/repo               # no selector = remote's default branch

Any git host works — apart from that GitHub-only shorthand, spm shells out to git and never detects or special-cases a provider, so GitHub, GitLab, Bitbucket, and self-hosted servers are all supported with no extra config:

spm add git@bitbucket.org:org/repo.git --branch main        # Bitbucket
spm add https://gitlab.com/org/repo.git --tag v1.0.0        # GitLab
spm add ssh://git@git.internal.example.com:7999/p/repo.git --branch main  # self-hosted

SSH auth goes through your ssh-agent / keys — spm never handles credentials. Private HTTPS repos use your git credential helper. spm runs git with GIT_TERMINAL_PROMPT=0, so a missing credential fails with a clear error instead of hanging on a prompt (helpers and ssh-agent still work).

Protocol override and opt-in fallback

By default spm uses the URL exactly as you gave it: an SSH URL is contacted over SSH and an HTTPS URL over HTTPS, and a failure is reported as-is. Two opt-in flags (accepted by the commands that can contact a remote: add, install, update, remove, link, unlink, target add and outdated) help when your preferred protocol isn't set up on the current machine, e.g. no SSH key in CI, or HTTPS blocked but SSH allowed:

spm add git@github.com:org/repo.git --branch main --protocol https   # force HTTPS
spm install --protocol-fallback                                      # try as given, then the other one
  • --protocol ssh|https rewrites the URL to that protocol before contacting the remote (https://host/org/repo <-> git@host:org/repo). URLs with no equivalent form (a port, file://, a local path, or a #fragment, ?query, percent-encoding, backslash or ~ path whose meaning differs between the two forms) are used as given.
  • --protocol-fallback retries once over the other protocol, and only after a connection or authentication failure — never after a missing ref or path. When it does, spm prints which protocol worked; if both fail, the error lists both attempts.

Neither flag changes what is recorded: ai.json and ai.lock keep the URL as recorded — the expanded https://github.com/<owner>/<repo>.git form for a github.com/... shorthand, otherwise exactly what you supplied; the protocol rewrite affects only the URL handed to git. Use the fallback deliberately: switching protocols can hide a real credential problem (an expired SSH key quietly replaced by an anonymous HTTPS clone that only works for public repos, or the reverse), which is why it is off by default.

Installation

spm ships as a single self-contained binary (needs the system git on PATH at runtime).

From npm (recommended) — the zero-setup path on every platform. It puts spm on your PATH with no manual steps:

npm i -g @camunda8/spm
spm --help

@camunda8/spm is a thin launcher that pulls in the matching prebuilt binary for your OS/CPU via an optional dependency (@camunda8/spm-<os>-<cpu>), so nothing is compiled or downloaded outside npm. Supported: darwin-x64, darwin-arm64, linux-x64, linux-arm64, win32-x64. Update with npm i -g @camunda8/spm@latest.

From crates.io — build and install from source via Cargo (needs a Rust toolchain). The crate is spm-cli; the installed binary is spm:

cargo install spm-cli

Prebuilt binary — download a release asset directly; no authentication required (the repo is public). The easiest way is the GitHub CLI:

# pick the asset for your platform (see list below); example: Apple Silicon macOS
gh release download --repo camunda/spm-cli \
  --pattern 'spm-aarch64-apple-darwin' --output spm
chmod +x spm && sudo mv spm /usr/local/bin/

--repo camunda/spm-cli with no tag grabs the latest release; add v0.1.0 as the first positional arg to pin a specific version.

Without gh, download straight from the public release URL with curl:

# latest release; swap the asset name for your platform
curl -fsSL -o spm \
  https://github.com/camunda/spm-cli/releases/latest/download/spm-aarch64-apple-darwin
chmod +x spm && sudo mv spm /usr/local/bin/

Assets: spm-x86_64-unknown-linux-gnu, spm-aarch64-unknown-linux-gnu, spm-x86_64-apple-darwin, spm-aarch64-apple-darwin, spm-x86_64-pc-windows-msvc.exe.

From source:

git clone https://github.com/camunda/spm-cli && cd spm-cli
make install                  # release build → /usr/local/bin/spm
make install PREFIX=~/.local  # or a custom prefix
# or: cargo install --path .

Commands

spm init [--target amp|claude|cline|codex|copilot|cursor|gemini|windsurf ...] [-g]  # scaffold ai.json (repeatable / comma-separated)
spm add <git> [--tag|--branch|--commit <v>|--version "<range>"] \  # add + install a skill (no selector = default branch; github.com/owner/repo@ref also works)
        [--path <subdir>] [--name <local-name>] [--all] [--force] [--lock-only] [-g]  # --all: add every skill under --path
        [--plugin]                                 # --plugin: add a full plugin (see "Full plugins")
spm target add [vendor ...] [-g]                   # add target vendor(s); no arg = pick interactively
spm target remove <vendor ...> [-g]               # drop target vendor(s) + clean their output; alias: rm
spm remove <name> [--plugin] [-g]                  # drop a skill (or a plugin with --plugin); alias: rm
spm update [name] [--lock-only] [-g]               # re-resolve branches/tags to latest; --lock-only: write ai.lock only
spm link <name> <dir> [-g]                         # develop a declared skill/plugin from a local dir (machine-local override)
spm unlink <name>|--all [-g]                       # drop link override(s), restore the locked version
spm install [-g] [--locked]                        # rebuild from ai.lock (after clone); alias: i; --locked fails if ai.lock is out of sync (CI)
spm list [-g] [--json]                             # show skills + pinned commits; alias: ls
spm status [-g] [--json]                           # check skills are materialized in this checkout
spm outdated [name] [-g] [--json] [--exit-code]    # report newer upstream versions (read-only)
spm doctor [-g] [--online] [--json]                # diagnose setup problems, with a fix per check (read-only)
spm clean [-g]                                     # remove generated vendor config
spm prune [--yes]                                  # wipe the global fetch cache ($SPM_HOME/store, default ~/.spm/store)
spm scan [path] [--within <dir>] [--json]          # scan skill content for suspicious patterns (default: .)

Four commands have short aliases that behave identically, including -g: spm i (install), spm rm (remove), spm ls (list) and spm target rm (target remove). add, install, update, remove, link, unlink, target add and outdated also accept --protocol ssh|https and --protocol-fallback (see Protocol override). add --force overwrites an existing entry of the same name instead of erroring. Run spm <command> --help for the authoritative, version-specific usage; the CLI reference covers every flag.

list, status, outdated and scan accept --json for scripts, CI and agents: stdout then holds exactly one JSON document with a top-level "schemaVersion": 1 and full commit SHAs, and exit codes are unchanged (the document is printed before a non-zero exit). See Machine-readable output for the shapes.

Developing a skill locally (spm link)

To try uncommitted changes to a skill inside a real consuming project, point the already-declared dependency at your working copy:

spm link reviewer ../my-skills/reviewer   # materialize the local dir instead of the locked commit
# ...edit ../my-skills/reviewer/SKILL.md, then:
spm install                               # re-copy the latest local content
spm unlink reviewer                       # back to the version pinned in ai.lock

The override lives under $SPM_HOME, never in the checkout (so a repository can't ship one); ai.json and ai.lock are never modified, so teammates and CI are unaffected. Linked content still goes through the content scan, and spm list / spm status show active links. See Authoring Skills.

Agent skill

This repo ships a user-facing skill, skills/spm/SKILL.md, that teaches an AI coding agent how to operate spm: the commands and flags, when to use which, and how to fix common failures. Install it with spm itself:

# In a project (needs an `ai.json` — run `spm init` first if the project has none):
spm add https://github.com/camunda/spm-cli --branch main --path skills/spm
# ...or for every project. Pass your agent's vendor to the global init — it defaults
# to `claude` (a repeated `init` is a no-op); to fix it afterwards, run
# `spm target add -g <vendor>` and `spm target remove -g claude`:
spm init -g --target copilot && spm add -g https://github.com/camunda/spm-cli --branch main --path skills/spm

Use --tag instead of --branch to pin a release that contains the skill. AGENTS.md and CLAUDE.md are for contributors to spm-cli, not for users.

Content scanning (security gate)

Skills are markdown + scripts that Claude/Copilot auto-discover and act on, so a malicious or compromised skill repo could smuggle in a payload that hijacks the agent or exfiltrates secrets. spm runs a deterministic content scan over every fetched skill/plugin before it is materialized into an agent-discovered directory. It flags:

  • Prompt injection — "ignore previous instructions", "disregard your system prompt", etc.
  • Secret/credential exfiltration — references to ~/.ssh/id_rsa, .aws/credentials, .git-credentials, GITHUB_TOKEN, … (escalated when paired with an outbound command).
  • Obfuscation — zero-width/bidi Unicode control characters, base64/hex blobs that decode to shell commands, and files padded past the 8 MiB scan cap (which would otherwise hide content behind a truncated read).
  • Command execution / network exfil — curl | bash, /dev/tcp/… and nc -e reverse shells.
  • Path traversal — ../../ requested in skill text.
  • Auto-run triggers — postinstall scripts, git hooks, bundled Makefiles.

High/critical findings block every command that syncs skills (add, install, update, remove, link, unlink and target add). To review a source (or gate it in CI) run it standalone — it exits non-zero on any blocking finding:

spm scan               # scan the current directory
spm scan ./my-skill    # scan a specific path

To override the gate for content you trust (or a false positive), set SPM_ALLOW_SUSPICIOUS=1 — findings are then printed as warnings but never block.

CI & automated updates (GitHub Actions)

Install spm in a workflow (checksum-verified, Linux/macOS/Windows) and open one pull request per outdated skill on a schedule — each with the version change and the spm scan findings of the new code:

# .github/workflows/spm-update.yml
on:
  schedule: [{ cron: "0 6 * * 1" }]
  workflow_dispatch:
permissions:
  contents: write
  pull-requests: write
concurrency: { group: "spm-update-${{ github.ref }}", cancel-in-progress: false }
jobs:
  update:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v5
      - uses: camunda/spm-cli@v0          # installs spm
      - uses: camunda/spm-cli/update@v0   # one PR per outdated skill/plugin

Blocking scan findings label the PR spm-scan-blocked; closing a PR skips that version. See CI & Automated Updates for the inputs, tokens (PRs opened with github.token don't trigger CI) and a Renovate alternative.

Global skills (-g / --global)

By default every command operates on the project in the current directory. Scope-aware commands (init, add, remove, update, install, link, unlink, list, status, outdated, clean, target add, target remove) accept -g (--global) to instead manage a user-global set of skills that is available to your AI tools in every project. spm prune and spm scan aren't scope-aware: prune always wipes the shared global fetch cache and scan always operates on a path.

spm init -g --target copilot                       # create the global manifest ($SPM_HOME/ai.json)
spm add  -g <git> --tag v1.0.0 --name reviewer     # install a skill globally
spm list -g                                        # list global skills
spm remove -g reviewer                             # drop a global skill
spm clean  -g                                      # remove global vendor config
  • The global manifest + lock live under $SPM_HOME (default ~/.spm/ai.json and ~/.spm/ai.lock) — commit/sync them with your dotfiles for a reproducible personal setup. They reuse the same fetch cache as project installs.
  • Where global skills materialize:
    • Copilot CLI → ~/.copilot/skills/<name>/ (its personal-skills dir). This directory is shared with skills you author by hand, so spm only ever touches the entries it manages and never wipes the whole directory.
    • Gemini CLI → ~/.gemini/skills/<name>/ (its user-skills dir). Also shared with your own hand-authored skills, so spm touches only its managed entries and never wipes the directory.
    • Codex CLI → ~/.agents/skills/<name>/ (the cross-tool user alias). Also shared, so spm touches only its managed entries and never wipes the directory.
    • Cursor → ~/.cursor/skills/<name>/ (its user-skills dir). Also shared, so spm touches only its managed entries and never wipes the directory.
    • Cline → ~/.cline/skills/<name>/ (its user-skills dir). Also shared, so spm touches only its managed entries and never wipes the directory.
    • Windsurf → ~/.codeium/windsurf/skills/<name>/ (its user-skills dir — note the ~/.codeium/windsurf path, not ~/.windsurf). Also shared, so spm touches only its managed entries and never wipes the directory.
    • Amp → ~/.config/agents/skills/<name>/ (its user-skills dir). Also shared, so spm touches only its managed entries and never wipes the directory.
    • Claude → a self-contained marketplace under $SPM_HOME/claude-global/, registered in ~/.claude/settings.json under the marketplace name spm-global (skills invoked as /spm-global:<name>). A distinct name keeps it from colliding with a project's spm marketplace.
  • A skill installed in both scopes collides by name at discovery time for the shared-dir targets (Copilot, Gemini, Codex, Cursor, Cline, Windsurf, Amp — a duplicate foo dir). Claude doesn't collide: /spm:foo (project) and /spm-global:foo (global) are distinct, namespaced commands. Either way, spm status warns on a same-name skill across scopes, since it compares the names recorded in each scope's ai.lock.

Worktrees & fresh clones

spm materializes skills into gitignored project-local dirs (.spm/claude/, .agents/skills/spm-managed-skills/). Git worktrees have their own working tree and don't share those untracked files, so — exactly like node_modules — each checkout needs its own spm install:

git worktree add ../feature -b feature
cd ../feature && spm install         # materialize this worktree's skills

Skipping this is the usual reason an agent doesn't see a declared skill in a new worktree or a fresh clone. spm status tells you at a glance and exits non-zero when anything is missing, so it works in scripts too:

spm status
# [claude]  0/1 installed  .../.spm/claude/plugin/skills
#   reviewer  MISSING
# error: some declared skills are not materialized in this checkout — run `spm install` here

To install automatically on every branch checkout and new worktree, add a post-checkout git hook (worktrees share the repo's .git/hooks):

# .git/hooks/post-checkout   — then: chmod +x .git/hooks/post-checkout
#!/bin/sh
# Re-materialize spm skills so Claude/Copilot always see the declared set.
[ -f ai.lock ] && command -v spm >/dev/null 2>&1 && spm install >/dev/null 2>&1
exit 0

Claude note: spm install writes the absolute path of the current checkout's .spm/claude/ into that checkout's .claude/settings.local.json. Since that file is gitignored, a new worktree either has no registration at all or — if it was copied over — one still pointing at the checkout it came from. Either way, run spm install inside the worktree and start (or /reload-plugins in) the Claude session from that same worktree; discovery is snapshotted at session start. spm status reports a stale pointer explicitly:

  ! .claude/settings.local.json marketplace points at /repo/.spm/claude, not this checkout (/repo-feature/.spm/claude)

To see what each harness actually loaded: claude plugin list / claude plugin marketplace list for Claude; copilot skill list for Copilot.

Design notes

  • Cross-OS: shells out to the system git (no libgit2 build deps); no symlinks; all paths via std::path. Runs on Linux, macOS, Windows.
  • SPM_HOME overrides spm's home (default ~/.spm) — used by tests. It holds the fetch cache (store/), the global-scope ai.json/ai.lock, machine-local spm link overrides (links/, links.json), the default policy.json, and the global Claude marketplace (claude-global/). Project-scope vendor output is always project-local and is not affected by SPM_HOME.
  • SPM_POLICY names the machine source-policy file (default $SPM_HOME/policy.json); see allowedSources.
  • Vendor adapters: adding a target means implementing one Vendor trait (src/vendor/). claude assembles a plugin-marketplace layout (marketplace.json → plugin.json → skills/<name>/SKILL.md) into the gitignored project-local .spm/claude/ and points to it; copilot copies skills into the gitignored project-local .agents/skills/spm-managed-skills/; the remaining targets (gemini, codex, cursor, cline, windsurf, amp) copy skills one level deep into a shared, team-committable skills dir via the generic src/vendor/shareddir.rs adapter — each is just a config row naming its project/global dirs (which may differ, e.g. Windsurf's ~/.codeium/windsurf/skills and Amp's ~/.config/agents/skills). All share the src/vendor/dirskills.rs copy/remove helpers and keep their materialized files out of VCS via the shared src/gitignore.rs helper.

Development

make check runs the full CI gate locally (fmt-check + clippy + test + test-action).

To cut a release, bump the crate version (the single source of truth for crates.io, npm, and the GitHub Release) with make bump — PART=patch|minor|major (default patch) or VERSION=X.Y.Z. Since main is protected, make bump-pr does the bump on a branch and opens the PR for you. See RELEASE.md for the full procedure.

A pre-commit hook (fmt + clippy) installs itself automatically via cargo-husky — just run cargo test (or cargo build) once after cloning and the hook lands in .git/hooks. The hook source lives in .cargo-husky/hooks/. Bypass a single commit with git commit --no-verify.

About

A command line tool that helps you manage external skills in your project as dependencies

Resources

Code of conduct

Security policy

Stars

8 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages