# 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](https://skills.sh) lists. skillcask does not need PHP,
Composer or Node.
## Install
**macOS, Linux**
```sh
brew install puniserv/tap/skillcask
# or, without Homebrew:
**Windows (PowerShell)**
```powershell
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**
```sh
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**
```sh
cargo install --locked skillcask
```
**Release archives** for macOS, Linux and Windows are on the
[releases page](https://github.com/puniserv/skillcask/releases), with sha256 checksums and GitHub
build provenance attestations:
```sh
gh attestation verify skillcask-x86_64-unknown-linux-gnu.tar.xz -R puniserv/skillcask
```
## Use
```sh
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
```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
| `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](https://skills.sh) lists skills as `owner/repo/skill`, for example
`mattpocock/skills/grill-me`. Put `github:` in front and add it:
```sh
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`:
```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](https://github.com/netresearch/composer-agent-skill-plugin)) 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
```sh
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](docs/spec.md).
## License
MIT OR Apache-2.0.