A CLI tool for packaging, pushing, and managing AI agent skills as OCI artifacts, following the Agent Skills OCI Artifacts Specification.
Built with Bubble Tea for an interactive terminal experience.
cd ~ && brew install liatrio/taproom/skills-oci && cd -brew upgrade skills-oci picks up new releases automatically.
go install github.com/liatrio/skills-oci@latestgit clone https://github.com/liatrio/skills-oci.git
cd skills-oci
go build -o skills-oci .A skill is a directory containing a SKILL.md file with YAML frontmatter that describes what the skill does, along with optional supporting files like scripts and references. Here is an example skill directory:
my-skill/
SKILL.md
scripts/
create-pr.sh
references/
REFERENCE.md
The SKILL.md file uses YAML frontmatter to declare metadata:
---
name: manage-pull-requests
version: 1.0.0
description: A skill for managing pull requests using the forgejo-cli.
license: Apache-2.0
compatibility: |
Requires forgejo-cli.
Agent must have network access to the Forgejo API.
metadata:
category: development-tools
tags: [git, forgejo, pull-requests, automation]
---
# Manage Pull Requests
Instructions and documentation for the skill go here...The push command packages a skill directory into an OCI artifact and pushes it to a container registry. The CLI reads the SKILL.md frontmatter to build the artifact config and annotations automatically.
See examples/package-and-push/ for a complete walkthrough using the popular pdf skill from skills.sh.
skills-oci push ghcr.io/myorg/skills/my-skill:1.0.0 ./my-skillskills-oci push localhost:5000/my-skill:1.0.0 ./my-skill --plain-httpIf no tag is provided in NAME[:TAG], the artifact is tagged as latest.
The CLI creates a standard OCI artifact with:
- Config blob (
application/vnd.agentskills.skill.config.v1+json) — JSON metadata extracted from the SKILL.md frontmatter (name, version, description, license, compatibility, etc.) - Content layer (
application/vnd.agentskills.skill.content.v1.tar+gzip) — A deterministic tar.gz archive of the skill directory, rooted at<skill-name>/ - Annotations — Standard OCI annotations (
org.opencontainers.image.title,.version,.created,.licenses) plus skill-specific ones (io.agentskills.skill.name)
The artifact is compatible with any OCI-compliant registry (GHCR, ECR, GAR, ACR, Docker Hub, Harbor, etc.).
The add command pulls a skill artifact from a registry, extracts it into .agents/skills/, creates symlinks in .claude/skills/, .codex/skills/, .cursor/skills/, and .gemini/skills/, and updates the project manifest files.
skills-oci add ghcr.io/myorg/skills/my-skill:1.0.0skills-oci add localhost:5000/my-skill:1.0.0 --plain-httpskills-oci add ghcr.io/myorg/skills/my-skill:1.0.0 --output ./custom/skillsAfter installation, the skill is extracted and ready for use:
my-project/
.agents/
skills/
manage-pull-requests/
SKILL.md
scripts/
create-pr.sh
skills.json
skills.lock.json
The CLI automatically manages two manifest files in your project directory:
A declarative manifest that declares which skills your project requires. It is created and updated automatically when you run skills-oci add or skills-oci remove.
{
"skills": [
{
"name": "manage-pull-requests",
"source": "ghcr.io/myorg/skills/manage-pull-requests",
"version": "1.0.0"
},
{
"name": "go-pro-skills",
"source": "ghcr.io/myorg/skills/go-pro-skills",
"version": "2.0.0"
}
]
}Each entry contains:
| Field | Required | Description |
|---|---|---|
name |
Yes | Skill identifier used for local references |
source |
Yes | OCI repository reference (without tag or digest) |
version |
No | OCI tag to install (should follow semver) |
A lock file that records the exact OCI digests and metadata of installed skills, ensuring reproducible installs across environments. This file should be committed to version control.
{
"lockfileVersion": 1,
"generatedAt": "2026-04-02T08:11:09Z",
"skills": [
{
"name": "manage-pull-requests",
"path": ".agents/skills/manage-pull-requests",
"source": {
"registry": "ghcr.io",
"repository": "myorg/skills/manage-pull-requests",
"tag": "1.0.0",
"digest": "sha256:bc6708cbbc37adb919157f04d31e601e68f4b9c24b35c655079da87ad0e30f86",
"ref": "ghcr.io/myorg/skills/manage-pull-requests:1.0.0@sha256:bc6708cb..."
},
"installedAt": "2026-04-02T08:11:09Z"
}
]
}The lock file pins each skill to an immutable digest, so installations are reproducible regardless of whether mutable tags (like latest or 1.0) have been updated.
skills-oci remove --name manage-pull-requestsThis removes the skill from skills.json, skills.lock.json, and deletes the extracted directory.
skills-oci catalog add records one or more third-party skills in a vendored.json desired-state file. It resolves an upstream GitHub reference to an immutable commit SHA, verifies that the upstream content actually contains a SKILL.md, and upserts an entry per skill. It never contacts the destination registry.
Single skill vs. discovery. If the URL/subpath points directly at a skill directory (one containing SKILL.md), exactly that skill is vendored — the classic single-skill case. If it points at a container (a directory or whole repo with no SKILL.md of its own), catalog add recursively discovers every directory that contains a SKILL.md and vendors each one, auto-naming it from its directory. Discovery stops descending once it finds a skill, so a skill's own nested example skills are not vendored separately. In discovery mode, --name does not apply (each skill is named from its directory).
Accepted URL forms. A directory at a ref (.../tree/<ref>/<subpath>), a repo root at a ref (.../tree/<ref>), or a bare repo (https://github.com/<owner>/<repo>). For a bare repo the default branch is resolved to its head commit and that SHA is recorded as the entry's commit pin.
Recorded fields. Each entry pins name, namespace, repo, subpath, and commit (the resolved 40-hex SHA). When the upstream SKILL.md declares a license, it is copied verbatim into an optional license field; entries whose upstream skill declares no license omit it. The destination OCI ref is not stored: the registry host and base namespace are owned by the consumer (skills-platform), which derives the ref as <writeRepo>/<repo>/<name> from its own config at processing time.
Source-qualified namespacing. Vendored skills are namespaced by their source repository so two upstreams that ship a skill with the same name never collide. The catalog namespace = <owner>-<repo> normalized to a single kebab token, e.g. mattpocock-skills. If two distinct source repos normalize to the same (namespace, name) but a different source repo, the add fails rather than silently overwriting the other source.
Re-adding a skill that is already listed overwrites its pin in place (matched by (namespace, name)). On an interactive terminal you are prompted per skill before each overwrite; new skills are added without prompting. In a non-interactive context (--plain, piped/no TTY) an overwrite requires -y/--yes — otherwise the command exits non-zero, naming the conflicts, without writing.
# Single skill, URL form (tag)
skills-oci catalog add https://github.com/anthropics/skills/tree/v1.0.0/skills/skill-creator
# Single skill, URL form (branch) — the branch head is resolved and the resulting SHA is recorded as the row's commit pin
skills-oci catalog add https://github.com/anthropics/skills/tree/main/skills/skill-creator
# Many skills — repo root at a commit: discovers every SKILL.md directory and vendors each
skills-oci catalog add https://github.com/anthropics/skills/tree/da20c92503b2e8ff1cf28ca81a0df4673debdbf7
# Many skills — bare repo: resolves the default branch, then discovers all skills
skills-oci catalog add https://github.com/vercel-labs/agent-skills
# Many skills under a container subpath
skills-oci catalog add https://github.com/anthropics/skills/tree/v1.0.0/skills
# Flag form (single skill)
skills-oci catalog add --repo anthropics/skills --subpath skills/skill-creator --version v1.0.0
# Overwrite existing entries non-interactively (CI / --plain) — overwrites every discovered skill
skills-oci catalog add --plain -y https://github.com/anthropics/skills/tree/v1.1.0
# Dry run prints the would-be entries without writing vendored.json
skills-oci catalog add <URL> --dry-run| Flag | Description |
|---|---|
--repo |
Upstream <owner>/<repo> slug (flag form; mutually exclusive with the positional URL) |
--subpath |
Path within the upstream repo. A skill directory vendors that one skill; a container directory discovers and vendors every skill beneath it |
--version |
Upstream tag, branch, or 40-hex commit SHA to vendor. Resolved to an immutable commit SHA, which is recorded as the entry's commit pin. Omit (bare-repo URL only) to resolve the default branch |
--name |
Local entry name (single-skill only; default: last segment of the upstream subpath). Ignored — and rejected — in discovery mode |
--vendored |
Path to vendored.json (default vendored.json) |
-y, --yes |
Overwrite existing entries without prompting (required to overwrite under --plain / non-interactive) |
--timeout |
Maximum time for the network-bound resolve + checkout steps (default 60s) |
--dry-run |
Print the would-be entries (and whether each would add or overwrite) and exit without writing |
skills-oci can be configured as a Claude Code SessionStart hook so that skills are automatically installed every time a Claude Code session starts. This means your project's skills are always present without any manual steps.
- Declare skills in
skills.jsonat the root of your project. - Register the hook using
skills-oci register. This writes aSessionStarthook into.claude/settings.jsonthat runsskills-oci install --plainon every session start. - Start Claude Code — the hook fires, reads
skills.json, and pulls any missing skills into.claude/skills/. Skills already present are skipped, so subsequent starts are fast.
Project start → Claude Code launches
│
▼
SessionStart hook fires
│
▼
skills-oci install --plain
│
reads skills.json
│
┌─────────┴──────────┐
▼ ▼
skill missing? already present?
pull from registry skip
│
extract to .claude/skills/
Step 1 — Install skills-oci
cd ~ && brew install liatrio/taproom/skills-oci && cd -
skills-oci --helpSee Installation for alternative install methods (go install, source).
Step 2 — Declare your skills
Create a skills.json in your project root (or add skills interactively via skills-oci add <NAME[:TAG]>):
{
"skills": [
{
"name": "manage-pull-requests",
"source": "ghcr.io/liatrio/skills/manage-pull-requests",
"version": "1.0.0"
}
]
}Step 3 — Register the hook
skills-oci registerThis creates or updates .claude/settings.json:
{
"hooks": {
"SessionStart": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "/usr/local/bin/skills-oci install --plain",
"timeout": 30
}
]
}
]
}
}Step 4 — Commit both files
git add skills.json skills.lock.json .claude/settings.json
git commit -m "add skills-oci hook for Claude Code"Any team member who clones the repo and opens Claude Code will automatically get the skills installed on their first session start.
See examples/claude-code-hooks/ for a minimal project showing the skills.json and the resulting .claude/settings.json.
To add or update a skill, run skills-oci add <NAME[:TAG]> (or edit skills.json directly) and commit the updated manifest. The hook will install the new skill on the next session start.
To remove a skill:
skills-oci remove --name manage-pull-requestsBy default, the CLI runs with an interactive terminal UI that shows progress through each phase with spinners and styled output. To disable the TUI (for CI/CD pipelines or scripting), use the --plain flag:
skills-oci push ghcr.io/myorg/skills/my-skill:1.0.0 ./my-skill --plain
skills-oci add ghcr.io/myorg/skills/my-skill:1.0.0 --plain| Flag | Description |
|---|---|
--plain |
Disable interactive TUI, use plain text output |
--plain-http |
Use HTTP instead of HTTPS for registry connections |
The CLI uses your existing Docker credentials from ~/.docker/config.json and any configured credential helpers. Log in to your registry before pushing or pulling:
# GitHub Container Registry
echo $GITHUB_TOKEN | docker login ghcr.io -u USERNAME --password-stdin
# Docker Hub
docker login
# AWS ECR
aws ecr get-login-password | docker login --username AWS --password-stdin <account>.dkr.ecr.<region>.amazonaws.comskills-oci reports a single skill.downloaded event after every
successful skill pull from add or install so the project can see real
adoption signal. Emission is best-effort and non-blocking: failures never
fail your command, time out beyond 2 seconds, or print errors to your
terminal.
| Field | Example | Why |
|---|---|---|
skill.namespace, name, version, digest, oci_ref |
the skill you pulled | adoption per skill |
client.{name,version,os,arch} |
skills-oci, 0.1.0, darwin, arm64 |
which CLI build is in use |
source.{command,trigger} |
add/install, user/manifest |
how the pull was initiated |
event_id, occurred_at |
a ULID, RFC 3339 UTC | idempotency + timing |
See docs/telemetry-data-contract.md
for the canonical wire shape.
- File paths, working directory, hostname, file contents,
SKILL.mdbodies. - Environment variables (other than the explicit telemetry config below).
- Registry credentials, GitHub tokens, or any other secret.
- Raw user identifiers (GitHub login, email). The forward-looking
actor.id_hashis a SHA-256 of the underlying value; the raw value is never transmitted.
To disable telemetry, set the env var to the exact value off:
export SKILLS_OCI_TELEMETRY=offAny other value (including unset) leaves telemetry on.
| Variable | Default | Effect |
|---|---|---|
SKILLS_OCI_TELEMETRY |
on |
off disables emission. Any other value (including unset) leaves it on. |
SKILLS_OCI_TELEMETRY_ENDPOINT |
compiled-in via -ldflags (empty in stock builds) |
Full URL of the collector, including /v1/events. Overrides the compiled-in default. |
SKILLS_OCI_TELEMETRY_TOKEN |
compiled-in via -ldflags (empty in stock builds) |
Bearer token sent in the Authorization header. Overrides the compiled-in default. |
Failed sends are appended to
<UserCacheDir>/skills-oci/telemetry/pending.ndjson (capped at 1 MB) and
drained on the next successful call, so transient collector outages do not
lose events.
A stdlib-only Python collector lives at
scripts/dev-collector.py for verifying the
producer end-to-end against the wire contract. Run it in one terminal,
point SKILLS_OCI_TELEMETRY_ENDPOINT at it from another, and exercise
add/install:
# terminal A
python3 scripts/dev-collector.py
# terminal B
export SKILLS_OCI_TELEMETRY_ENDPOINT=http://127.0.0.1:8787/v1/events
export SKILLS_OCI_TELEMETRY_TOKEN=dev-token
./skills-oci add <registry>/<namespace>/<skill>:<tag> --plainFlags on the collector exercise specific paths:
--fail-first N (transient → buffer → drain), --status 400 (4xx →
last-error.log, no buffer growth), --require-bearer TOKEN (auth
header). See the script's docstring for the full list.