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
# or, without Homebrew:
|
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
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
Release archives for macOS, Linux and Windows are on the releases page, with sha256 checksums and GitHub build provenance attestations:
Use
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
[]
= { = "composer", = "https://git.example.com/api/v4/group/42/-/packages/composer/packages.json" }
# A repository you review yourself can skip the cooldown; the others keep it.
= { = "https://git.example.com/own/packages.json", = "0" }
# Default sources, written by `init`; `= false` turns one off. Packagist is always searched last.
= { = "https://repo.packagist.org" }
= { = "github", = "https://api.github.com" } # GitHub Enterprise: its API URL
[]
= "^3.34"
= { = "^1.0", = "acme" }
= { = "1.4.2", = "acme", = ["review"] }
= { = true, = "^1.3", = ["tdd", "code-review"], = { = "mp-code-review" } }
= { = true, = ["pdf"] } # default branch
[]
= [".claude/skills", ".junie/skills"]
= ".skills" # project skills
[]
= "3d" # cooldown for add/update; "0" disables
= true # false: every version must be exact
= true # false: drop the implicit Packagist repository
= false # true: dependencies may come from GitHub
= ["acme", "own", "packagist", "github"] # only these sources; default: all
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).
Allowed sources and policy for every project
allow-sources lists the only places skills may come from: repository names, packagist,
github, or https:// URL prefixes (https://api.github.com/repos/acme/ allows one GitHub
organisation). Other repositories get no request, and install refuses a lock entry from them,
also with --frozen, so a committed lock cannot bring them back. Dependencies follow the same
rule.
Policy can also come from config.toml next to the global manifest (~/.config/skillcask) and
from the environment. The first layer that sets a key wins:
SKILLCASK_ALLOW_SOURCES,SKILLCASK_MIN_RELEASE_AGE,SKILLCASK_ALLOW_RANGES,SKILLCASK_PACKAGIST,SKILLCASK_GITHUB_REQUIRES(overriding an enforced key prints a warning);- keys
config.tomllists inenforce: a project that sets them otherwise fails; [policy]inskills.toml;- the other keys of
config.toml, as defaults.
# ~/.config/skillcask/config.toml: only the company registry and GitHub, in every project
[]
= ["https://gitlab.example.com/", "github"]
= ["allow-sources"]
config.toml and the environment take URL prefixes, not repository names, since a name means
whatever a project calls it. skillcask status prints each key that is set and where it came
from.
Dependencies
A skill that needs other skills lists them in its SKILL.md:
---
name: gitlab-branch-mr
version: 1.2.0
requires:
acme/security-review: ^1.0
github:mattpocock/skills/pr: main
---
add, install and update resolve, lock and install them like your own entries: same
cooldown, same hashes, one version per package (the highest one every requirer accepts). They
stay out of skills.toml; the lock records each package's requires and switches to
version = 2. A dependency is looked up only in your repositories, in order, then Packagist;
a github: one needs github-requires = true. remove drops dependencies nobody needs any
more, and status shows them as via <requirer>.
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:
= { = true, = { = "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:
skills.toml,skills.lockand an optionalauth.jsonlive in~/.config/skillcask($XDG_CONFIG_HOME/skillcask;SKILLCASK_HOMEoverrides both). Keep them in your dotfiles.- Skills go to
~/.agents/skills(read by Codex), with links in~/.claude/skillsand~/.junie/skills([agents] targets, relative to home). Links are absolute, so a symlinked~/.claudeworks.[agents] localdoes 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 skill packages of the other repositories, GitLab included. |
auth set|remove <host> |
Keep a registry token in git's credential helper (Keychain, Credential Manager, libsecret). |
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.
Output is coloured in a terminal (NO_COLOR turns it off, CLICOLOR_FORCE on) and errors name
the next step on a hint: line. For coding agents, --agent or SKILLCASK_AGENT=1 prints terse,
uncoloured, tab-separated output with only the rows that need action; it is on by itself when
AI_AGENT or CLAUDECODE=1 is set, unless SKILLCASK_AGENT=0.
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:
All skills of one repository share one entry in skills.toml:
= { = true, = "^1.3", = ["grill-me", "tdd"] }
Which version you get:
- Repository with version tags (
v1.3.1):addwritesversion = "^<major.minor>", like for Composer packages, andupdatemoves 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, soinstallkeeps installing that commit until you runupdate. @picks explicitly:github:owner/repo@^2.0(version tags),github:owner/repo@mainor@<commit sha>(ref).- With
policy.allow-ranges = falsea branch counts as a range:addwrites the full commit sha asref, 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.
installverifies 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_FILEreplaces 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-agefor GitHub sources uses times GitHub sets: release publication for tags, push times from the Activity API for branches. A tag without a release, or a ref GitHub lists no pushes for, falls back to commit dates, which the committer sets; reviewing whatadd/updatechange inskills.lockis the real check there.- Credentials come from Composer's
auth.json/COMPOSER_AUTH(http-basic,bearer,gitlab-token,gitlab-oauth), then from git's credential helper, and are never printed.skillcask auth set git.example.comkeeps a token in the macOS Keychain, the Windows Credential Manager or libsecret instead of a plain-text file (the default usernamePRIVATE-TOKENsends GitLab's header;--username oauth2a Bearer token).SKILLCASK_GIT_CREDENTIALS=0turns the helper off. In GitLab CI usehttp-basicwith the usergitlab-ci-tokenand 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
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:
--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.