skillcask 0.1.1

A lockfile-first package manager for AI agent skills (SKILL.md), backed by Composer repositories
skillcask-0.1.1 is not a library.

skillcask

A lockfile-first package manager for AI agent skills: directories with a SKILL.md, read by Claude Code, Codex, Junie and others.

skillcask installs skills the way Cargo or Composer install libraries. It reads a manifest with version constraints and commits a lock with content hashes. In CI it does frozen installs that fail on drift, and it links every skill into each agent's skill directory. It ships as a single static binary with no runtime, so it works in a repository in any language.

Skills come from Composer repositories or straight from GitHub. Packagist serves public skill packages; GitLab's Composer registry, Satis, Private Packagist or the static repository built by skillcask publish serve private ones. Any GitHub repository with SKILL.md files works too, which covers most of what skills.sh lists. skillcask does not need PHP, Composer or Node.

Install

macOS, Linux

brew install puniserv/tap/skillcask
# or, without Homebrew:
curl --proto '=https' --tlsv1.2 -LsSf https://github.com/puniserv/skillcask/releases/latest/download/skillcask-installer.sh | sh

Windows (PowerShell)

powershell -ExecutionPolicy Bypass -c "irm https://github.com/puniserv/skillcask/releases/latest/download/skillcask-installer.ps1 | iex"

The two installer scripts download the binary for your platform from the latest release and put it in ~/.cargo/bin (%USERPROFILE%\.cargo\bin on Windows).

npm, bun

npm install -g skillcask      # global
bun add -g skillcask          # global, bun
npx skillcask install --frozen
bunx skillcask install --frozen

The npm package is a thin wrapper that downloads the release binary for your platform from GitHub. It does not check a checksum itself; where that matters, use Homebrew or a release archive and verify it.

From source

cargo install --locked --git https://github.com/puniserv/skillcask

Release archives for macOS, Linux and Windows are on the releases page, with sha256 checksums and GitHub build provenance attestations:

gh attestation verify skillcask-x86_64-unknown-linux-gnu.tar.xz -R puniserv/skillcask

Use

skillcask add netresearch/agent-jira-skill   # resolve, write skills.toml + skills.lock, install
skillcask add github:mattpocock/skills/tdd   # one skill from a GitHub repository
skillcask install --frozen                   # CI: install exactly the lock or exit non-zero
skillcask status                             # manifest vs lock vs installed files

add creates:

skills.toml                  # manifest, commit it
skills.lock                  # lock, commit it
.agents/skills/<name>/       # installed skill (gitignored)
.claude/skills/<name>  -> ../../.agents/skills/<name>
.junie/skills/<name>   -> ../../.agents/skills/<name>
.gitignore                   # managed block

Project skills live in .skills/<name>/SKILL.md ([agents] local). skillcask links each one into the store (.agents/skills/<name> -> ../../.skills/<name>) and from there into every agent directory. These links are committed, so they work in every clone. A directory written by hand straight into .agents/skills/ is linked the same way. A project skill with the same name as a locked one is an error.

skills.toml

[repositories]
# Packagist is the implicit last repository.
acme = { type = "composer", url = "https://git.example.com/api/v4/group/42/-/packages/composer/packages.json" }

[skills]
"netresearch/agent-jira-skill" = "^3.34"
"acme/agents-md" = { version = "^1.0", repository = "acme" }
"acme/review" = { version = "1.4.2", repository = "acme", skills = ["review"] }
"mattpocock/skills" = { github = true, version = "^1.3", skills = ["tdd", "grill-me"] }
"anthropics/skills" = { github = true, skills = ["pdf"] }      # default branch

[agents]
targets = [".claude/skills", ".junie/skills"]
local = ".skills"        # project skills

[policy]
min-release-age = "3d"   # cooldown for add/update; "0" disables
allow-ranges = true      # false: every version must be exact
packagist = true         # false: drop the implicit Packagist repository

Constraints follow Composer semantics: ^0.3 means >=0.3.0 <0.4.0, ~1.2 means >=1.2 <2.0, and pre-releases are only selected when the constraint names a stability (^1.0@beta).

Commands

Command
init Create skills.toml.
add <vendor/name>[@constraint] [--repository r] Resolve, lock and install.
add github:<owner/repo>[/skill][@version-or-ref] The same from a GitHub repository, see below.
remove <vendor/name>… Drop from manifest and lock; delete files, links and gitignore lines.
install [--frozen] Install the lock. --frozen (implied by CI) fails when the lock is out of date.
update [vendor/name…] Re-resolve within constraints.
outdated Newer versions, and when cooled-down ones become eligible.
status Exit 1 when manifest, lock or installed files disagree.
doctor [--fix] Broken or committed links, stale gitignore block, modified skills.
search <query> Packagist (type=ai-agent-skill) and listed private packages.
publish --vendor v --base-url URL --out DIR [paths…] Build a static Composer repository from a monorepo.
<other> Runs skillcask-<other> from PATH, like git and cargo plugins.

Exit codes: 0 ok, 1 manifest/lock/status problems, 2 authentication, 3 network, 4 hash mismatch, 5 IO or archive error.

Skills from GitHub and skills.sh

skills.sh lists skills as owner/repo/skill, for example mattpocock/skills/grill-me. Put github: in front and add it:

skillcask add github:mattpocock/skills/grill-me   # one skill
skillcask add github:mattpocock/skills/tdd        # a second one from the same repository
skillcask add github:anthropics/skills            # every skill in the repository

All skills of one repository share one entry in skills.toml:

"mattpocock/skills" = { github = true, version = "^1.3", skills = ["grill-me", "tdd"] }

Which version you get:

  • Repository with version tags (v1.3.1): add writes version = "^<major.minor>", like for Composer packages, and update moves within it. The release time of a tag is its commit date.
  • Repository without tags: the entry follows the default branch; ref = "main" names a branch, tag or commit instead. The lock pins the commit sha, so install keeps installing that commit until you run update.
  • @ picks explicitly: github:owner/repo@^2.0 (version tags), github:owner/repo@main or @<commit sha> (ref).
  • With policy.allow-ranges = false a branch counts as a range: add writes the full commit sha as ref, and a hand-written branch name is rejected.

Skills are found by their SKILL.md anywhere in the repository, except inside another skill. The install name is the frontmatter name; when that is not a valid name (Jira Automation), the directory name is used. skills = [...] takes these names.

GitHub allows 60 anonymous API requests per hour. add, update and outdated use a few per entry; install from the lock uses none, it downloads the pinned archive. Set GITHUB_TOKEN (GitHub Actions has one) or Composer's "github-oauth": {"github.com": "<token>"} in auth.json for a higher limit and for private repositories. SKILLCASK_GITHUB_API points at GitHub Enterprise (https://ghe.example.com/api/v3).

skills.sh itself is not queried: its API needs a Vercel token. Search on the website, then add what you picked.

Security

  • The lock pins the version, the commit reference, the sha256 of the downloaded archive and the tree hash of every installed skill. install verifies both hashes, so what gets installed is what was reviewed.
  • HTTPS only, verified against the system trust store, so corporate CAs work. Verification cannot be turned off; SKILLCASK_CA_FILE replaces the trust store with a PEM bundle.
  • Archives with absolute paths, .., devices or symlinks that leave their skill are rejected. No symlink is ever written from a package. Size and file count are capped (--max-download, --max-unpacked, --max-files).
  • No package code runs, ever. No telemetry.
  • min-release-age for GitHub sources reads commit dates, which the committer sets. A backdated commit passes the cooldown; reviewing what add/update change in skills.lock is the real check there.
  • Credentials come from Composer's auth.json / COMPOSER_AUTH (http-basic, bearer, gitlab-token, gitlab-oauth) and are never printed. In GitLab CI use http-basic with the user gitlab-ci-token and the password $CI_JOB_TOKEN.

Publishing skills

One repository per skill: a composer.json with "type": "ai-agent-skill" (the netresearch convention) and semver tags. Packagist or GitLab's Composer registry picks up the tags; skillcask plays no part.

Monorepo: give each SKILL.md a version in its frontmatter and run

skillcask publish --vendor acme --base-url https://skills.example.com --out public skills/

This writes packages.json, p2/acme/<name>.json and deterministic zips under dist/. Published versions are immutable, and re-running on unchanged skills changes no file. Serve the directory over HTTPS.

Design

See docs/spec.md.

License

MIT OR Apache-2.0.