skillcask 0.1.2

A lockfile-first package manager for AI agent skills (SKILL.md), backed by Composer repositories
# 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:
curl --proto '=https' --tlsv1.2 -LsSf https://github.com/puniserv/skillcask/releases/latest/download/skillcask-installer.sh | sh
```

**Windows (PowerShell)**

```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**

```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

| 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](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.