-
Notifications
You must be signed in to change notification settings - Fork 0
Home
A package manager for agent skills: manifest for intent, lockfile pinned to commit SHAs, reproducible installs on any machine.
The popular tool in this space (Vercel's npx skills) records installations in
skills-lock.json, but that file stores only a normalized source
(owner/repo) and a content hash of the installed files — no git ref, no
commit SHA, and no separate user-intent manifest. It can detect that a skill
changed, but cannot reproduce an installation. Their own issue tracker
asks for exactly this (issues #283, #549).
Every mature dependency manager solves this with two files:
- a manifest — hand-written, expresses what the user wants;
- a lockfile — generated, pins exact versions (commit IDs for git sources).
Skiletto brings that model to agent skills.
| Decision | Choice |
|---|---|
| Language | Go — single small static binary |
| Distribution | Standalone binary via goreleaser (GitHub releases on v* tags); npm package skiletto (postinstall downloads the release binary) and PyPI package skiletto (platform wheels carrying the binary), publish steps gated on tokens |
| Git access | Shell out to system git — user's auth (SSH, .netrc, credential helpers) works for free. Shallow fetch + sparse checkout for cheap subdir installs. Library (go-git) is a possible later swap, not v1 — it has no credential-helper support and brings its own SSH stack, so auth would need reimplementing. |
| Manifest |
skiletto.toml (TOML, hand-editable) |
| Lockfile |
skiletto.lock (TOML, generated, committed to VCS) |
| Harness support | Adapter architecture; v1 ships only the Claude Code adapter |
| Drift policy | Warn and skip; --force overwrites |
A skill is a directory containing SKILL.md inside any git repo — GitHub,
private Gitea, anything system git can clone. No registry, no publishing step,
no extra metadata required. Skills with missing/partial frontmatter are
installed as-is (possibly with a warning); whether a harness accepts them is
the harness's business.
Skill name defaults to its directory name; collisions require an explicit rename in the manifest.
-
Project:
skiletto.toml+skiletto.lockin the repo root. Skills materialize in.agents/skills/<name>. -
Machine (global): manifest + lock in the platform config dir
(
~/.config/skiletto/on Linux,os.UserConfigDirequivalents elsewhere). Skills materialize in~/.agents/skills/<name>.
- Resolve the source ref to a commit SHA, fetch, and materialize the skill
directory in the canonical location for the scope (
.agents/skills/or~/.agents/skills/). - Each enabled harness adapter links individual skill directories (not the whole skills dir) into its expected location.
Adapter = knows the harness's skill directories per scope + how to link.
v1 adapter: claude — symlinks into .claude/skills/ (project) and
~/.claude/skills/ (machine).
Link strategies (shared link helper): unix uses symlinks only. Windows
tries symlink (needs Developer Mode) → directory junction → copy. The
strategy is derived at inspection time, never persisted: symlinks and
junctions self-identify as reparse points; a copy counts as ours only while
it hash-matches the canonical tree (a diverged copy is a local modification —
refused without --force). Pristine copies update/sync/remove exactly like
symlinks (links are cleared against the old canonical tree before the new
version is promoted). Editable installs need a live link, so they fail with
guidance on a copy-only filesystem.
Skills already present but not in the manifest are ignored (shown by
skiletto list, never touched).
Two distinct intents, both using a filesystem path as source:
-
Editable (
skiletto add --editable <path>, primary use case: developing a skill in a local clone and installing it machine-wide): the canonical location gets a symlink to the working tree, so edits are live. The lock entry is markededitable = truewith no commit and no content hash; drift checking is skipped — the working tree is expected to change. -
Pinned local (
skiletto add <path>without--editable): the path must be a git repo; treat it like any other git source — resolve the ref to a commit SHA against the local clone, copy the content out, lock the SHA.
Path sources are inherently machine-specific. They are the normal case in the
machine-scope manifest; in a project-scope manifest add warns (once per
invocation) that teammates' sync will fail on them. Relative paths are
absolutized against the invocation cwd at add time; ~/ is stored literally
and expanded at use time.
skiletto.toml:
[skills]
pdf = { source = "https://github.com/anthropics/skills", path = "skills/pdf", ref = "main" }
deploy = { source = "ssh://gitea@git.kumekay.com:30009/ku/skills.git", path = "deploy" }
my-skill = { source = "~/p/my-skills", path = "my-skill", editable = true }-
source— anything git can clone (stored as a full canonical URL) or a local filesystem path. CLI shorthands (owner/repo→https://github.com/owner/repo) are expanded byaddbefore writing;skiletto.tomlandskiletto.locknever contain shorthands or provider special cases. -
path— subdirectory of the skill inside the repo; omitted means repo root. -
ref— branch, tag, or SHA; omitted means the repo's default branch. -
editable— local path sources only; link the working tree instead of copying a pinned commit (see Local skills).
skiletto.lock:
version = 1
[[skill]]
name = "pdf"
source = "https://github.com/anthropics/skills"
path = "skills/pdf"
ref = "main"
commit = "8c1f2ab90d3e4f56a7b8c9d0e1f2a3b4c5d6e7f8"
hash = "sha256:…" # content hash of the installed tree, for drift detection| Command | Behavior |
|---|---|
skiletto add <source> [--global] [--editable] [--all] |
Add to manifest, resolve, lock, install, link. --editable requires a local path source. Multi-skill source without //path: interactive multi-select on a TTY, --all installs everything, otherwise an actionable error. Relative path sources are absolutized against the invocation cwd at add time (~/ stays literal). |
skiletto sync [--force] |
Make installed state match the lock exactly; resolve and lock anything unlocked. Never moves already-locked versions — including manifest entries edited while the installed tree is drifted (warn/skip without --force). Prunes only what it removes from the lock itself (in lock, gone from manifest) — and refuses to delete a drifted skill without --force. |
skiletto update [name] [--force] |
The only command that re-resolves refs and rewrites lock entries. No name = all. Editable entries are skipped with a note; drifted trees are skipped (exit 1) without --force. |
skiletto remove <name> [--force] |
Remove from manifest + lock, unlink, delete materialized copy. Editable: only the canonical link is removed, never the worktree. Drifted: refused without --force. |
skiletto list |
Managed skills (name, source, pinned commit or editable, status: ok / drifted / missing / not-locked), lock-only orphans as pruned on next sync, and unmanaged skills found in the canonical dir or any adapter dir. Observes only; always exits 0. |
skiletto import [path] [--global] [--force] |
Bootstrap skiletto.toml from an existing Vercel skills-lock.json (one-way): map each entry to a canonical source (github and git sourceTypes; others fail per-entry), resolve the default branch HEAD to a SHA, write a fully pinned skiletto.lock, install and link. Partial-but-honest: failures are reported per entry, exit non-zero. Existing manifest names are skipped; trees import cannot prove pristine are refused without --force; real dirs left in adapter dirs by npx skills are never touched (error names the dir and the rm -r remedy). |
All mutating commands take --global (machine scope) and --no-input
(root-level persistent flag).
Source spec for add: <repo>[//subdir][@ref], e.g.
skiletto add github.com/anthropics/skills//skills/pdf@main.
Non-interactive is the contract; interactive is sugar on top (the gh CLI
model):
- Every piece of information a prompt can collect has a flag or argument equivalent — scripts and CI never need a TTY.
- Prompts appear only when stdin/stdout are a TTY and the invocation is
ambiguous. Canonical case:
skiletto add owner/repoon a repo containing several skills → multi-select picker;--allinstalls everything,//pathpicks one explicitly. - No TTY + ambiguous invocation → exit non-zero with an actionable error listing the available skills and the exact syntax to script the choice.
-
--no-inputforces the non-interactive path even in a terminal; a setCIenv var implies it.
sync compares the lock's content hash with the installed tree. On mismatch:
warn, leave the files alone, continue with other skills, exit with a warning
status. sync --force restores the locked version. The same guard protects
every other path that would replace a drifted tree: manifest entries edited
while drifted, update, remove, and import all warn/skip (or fail the
entry) without --force. Editable entries are never drift-checked. On
Windows, a copy-linked adapter dir that no longer hash-matches counts as a
local modification under the same rules.
- No registry or publishing.
- No skill-to-skill dependencies.
- No bundled git implementation.
- No adopting/managing pre-existing unmanaged skills (list them only).
- No interop with
npx skillsbeyond one-wayimport.
-
Symlinked skill dirs— verified 2026-07-03: Claude Code loads a skill whose individual directory in.claude/skills/is a symlink (tested headless against a real-dir control). Whole-dir symlinks (~/.claude/skills → ~/.agents/skills) also known to work. Multi-level chains (adapter symlink → canonical symlink → editable working tree) should resolve at the filesystem level but haven't been explicitly tested. -
Windows— resolved 2026-07-04: symlink → junction → copy chain in the shared link helper (see Install model), exercised by a dedicated Windows CI job including a forced-junction and forced-copy e2e. Note: Go 1.23+ does not report junctions as symlinks, so link inspection routes through reparse-point-aware helpers. -
Whole-repo skills— works;pathis simply omitted. -
Sparse/shallow git flags— implemented with capability detection and a full-fetch fallback; hermetic tests run against local repos.
skiletto/
├── main.go # tiny: calls internal/cli (root main ⇒ go install github.com/kumekay/skiletto@latest)
├── internal/
│ ├── cli/ # cobra commands — thin: parse flags, call engine, format output
│ ├── engine/ # orchestration: diff(manifest, lock, disk) → Plan → apply
│ ├── manifest/ # skiletto.toml read/write + CLI source-spec parsing/expansion
│ ├── lockfile/ # skiletto.lock read/write
│ ├── source/ # Source interface + git and path implementations
│ ├── gitcli/ # system-git wrapper: exec, version/capability detection
│ ├── skill/ # SKILL.md discovery in a tree, naming, content hashing
│ ├── scope/ # project vs machine: manifest/lock/canonical-dir paths
│ ├── adapter/ # Adapter interface, registry, link helper w/ Windows fallback
│ │ └── claude/
│ ├── ui/ # Prompter interface: huh-backed TTY impl + non-interactive impl that errors
│ └── vercelimport/ # skills-lock.json reader (isolated, one-way)
├── packaging/
│ ├── npm/ # npm wrapper package (postinstall downloads release binary)
│ └── pypi/ # wheel builder bundling the release binary
└── .goreleaser.yaml
-
source.Source—Resolve(ref) → commit+Fetch(commit, subpath, dest). Implementations: git (viagitcli) and local path (pinned or editable). Later: other backends without touching the engine. -
adapter.Adapter—SkillsDir(scope),Link/Unlink(skill). Compiled-in registry (a map), no plugin system. New harness = new package + one registry line. Shared link helper owns the symlink/junction/copy decision. -
gitcliboundary — allexec gitcalls behind one small client, so a go-git swap or capability-based fallbacks (old git → full clone) stay local. -
ui.Prompter— commands never talk to a prompt library directly; the non-TTY implementation returns the actionable error. Enforces the "prompts are sugar for flags" contract structurally.
engine computes a Plan (list of actions: fetch, materialize, link,
unlink, prune, warn-drift) from manifest + lock + disk state, then applies
it. Buys --dry-run for free, makes list/status the same diff rendered
instead of applied, and lets tests assert on plans against fake
sources/adapters without touching the network or filesystem.
Lockfile format, the canonical store layout, and TOML parsing stay concrete — single implementations, no interfaces. Dependencies stay minimal: cobra, a TOML library, and (later, for pickers) charmbracelet/huh.
All v1 milestones shipped (2026-07-04):
-
(#9)add+sync, project scope, canonical dir + claude adapter -
Machine scope ((#10)--global) -
(#12)update,remove,listwith drift status -
(#13)importfromskills-lock.json -
Distribution: goreleaser → GitHub releases, npm wrapper, pip wrapper(#14; publishing awaits the firstv*tag +NPM_TOKEN/PYPI_TOKENsecrets)
Plus: interactive picker with --all/--no-input (#15), Windows link
fallback + Windows CI (#16).