skillcask 0.2.0

A lockfile-first package manager for AI agent skills (SKILL.md), backed by Composer repositories
skillcask-0.2.0 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.

Cargo

cargo install --locked 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)
.agents/skills/.skillcask-installed  # what this machine installed (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. When a skill with the same name exists in your home (~/.claude/skills, ~/.agents/skills, …), install and doctor print a warning: Claude Code runs the personal one, Codex lists both.

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", "code-review"], rename = { code-review = "mp-code-review" } }
"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).

Name conflicts

Skills have no namespaces: an agent knows a skill by its name. Two skills with the same name in one project are an error. Install one of them under another name with rename:

"mattpocock/skills" = { github = true, rename = { code-review = "mp-code-review" } }

skillcask then installs .agents/skills/mp-code-review and changes the name: line in its SKILL.md; the lock records the original name and the hash of the renamed copy. Nothing else in the skill changes, so text that mentions the old name (/code-review) stays as it is. add, install and update offer the rename when they hit a conflict in a terminal.

Personal skills (--global)

skillcask --global (-g) manages skills for your user instead of a project, with the same manifest, lock and verification:

skillcask -g add github:mattpocock/skills/grill-me
skillcask -g install --frozen    # e.g. from a dotfiles repository
  • skills.toml, skills.lock and an optional auth.json live in ~/.config/skillcask ($XDG_CONFIG_HOME/skillcask; SKILLCASK_HOME overrides both). Keep them in your dotfiles.
  • Skills go to ~/.agents/skills (read by Codex), with links in ~/.claude/skills and ~/.junie/skills ([agents] targets, relative to home). Links are absolute, so a symlinked ~/.claude works. [agents] local does not apply.
  • Personal skills you wrote by hand stay where they are and are not linked anywhere.
  • The list of managed paths is ~/.agents/skills/.skillcask-installed, local to the machine; nothing touches git.

Commands

Every command except publish takes -g, --global.

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 [--inline] [--dist-url TPL] [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).
  • File names must be valid on every OS: Windows-reserved names, <>:"|?*\, control characters and paths that collide by case or Unicode form fail a skill that contains them, and are skipped outside the selected skills.
  • 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.

Hosts that cannot serve / inside file names (GitLab's Generic Package Registry) take a single file instead:

skillcask publish --inline --vendor acme --out composer/ skills/ \
  --dist-url 'https://git.example.com/api/v4/projects/7/packages/generic/{vendor}-{name}/{version}/{name}-{version}.zip'

--inline puts every package into packages.json and writes no p2/. Upload each dist/<vendor>/<name>/<version>.zip to the URL the template gives, and commit packages.json to a repository. A repository url with a query string is used as given, so clients can read the file through the GitLab raw files API (.../repository/files/composer%2Fpackages.json/raw?ref=main) with a gitlab-token.

Design

See docs/specs/2026-10-19-skillcask-v1.md.

License

MIT OR Apache-2.0.