skiletto

command module
v0.3.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 19, 2026 License: MIT Imports: 2 Imported by: 0

README

skiletto

Package manager for agent skills: manifest for intent, lockfile pinned to commit SHAs, reproducible installs on any machine.

A skill is any directory containing a SKILL.md inside a git repository. skiletto records what you want in skiletto.toml, pins exact commits and content hashes in skiletto.lock (commit both), materializes skills in .agents/skills/<name>, and symlinks them into each enabled harness (e.g. Claude Code's .claude/skills/; run skiletto harness list for the full set). The canonical .agents/skills/ dir is always populated — harnesses that read it directly need no links at all; the rest are opt-in via skiletto harness.

Early development — see the design doc for where this is headed.

Install

npm install -g skiletto
# or
pip install skiletto

Or download a prebuilt binary for your platform from the releases page, or build from source with a Go toolchain:

go install github.com/kumekay/skiletto@latest

Usage

Run in your project root:

# add a skill: <repo>[//subdir][@ref]
skiletto add anthropics/skills//skills/pdf            # GitHub shorthand
skiletto add https://github.com/anthropics/skills//skills/pdf@main
skiletto add ssh://gitea@git.example.com:30009/me/skills.git//deploy
skiletto add ~/p/my-skills//my-skill                  # local git repo, pinned
skiletto add --editable ~/p/my-skills//my-skill       # link the working tree

# a pasted GitHub browser URL works too
skiletto add https://github.com/anthropics/skills/tree/main/skills/pdf

# a source with several skills and no //path: pick interactively, or
skiletto add --all anthropics/skills                  # install every skill in it
skiletto add --skill pdf --skill web anthropics/skills # just the named ones

# make installed skills match the lockfile exactly
skiletto sync
skiletto sync --force   # also restore drifted skills to their locked version

# re-resolve refs to the current commit and rewrite the lock
skiletto update         # every skill
skiletto update pdf     # just one
skiletto update --force # overwrite drifted skills too

# drop a skill from the manifest, lock, links, and disk
skiletto remove pdf
skiletto remove --force pdf   # remove even if it has local modifications

# show managed skills (with drift status) and unmanaged ones
skiletto list

# bootstrap from a Vercel `npx skills` skills-lock.json (one-way)
skiletto import                 # reads ./skills-lock.json
skiletto import path/to/skills-lock.json

# choose which harnesses get per-skill links (saved in skiletto.toml)
skiletto harness list                     # registered harnesses + where enabled
skiletto harness enable claude            # this project
skiletto harness enable claude --global   # machine-wide, applies in every project
skiletto harness disable claude

# --global (-g) installs machine-wide instead of into the current project
skiletto add --global --editable ~/p/my-skills//my-skill
skiletto sync -g
  • add resolves the ref (or the default branch) to a commit SHA, records the skill in skiletto.toml, pins commit and content hash in skiletto.lock, installs it, and links it into every harness. If the source contains several skills and no //path picks one, add shows a multi-select picker in a terminal and installs everything you check. --all installs every skill without prompting; --skill <name> (repeatable) installs just the named ones, and combines with a //path that narrows where to look. With no TTY — or with --no-input, or when the CI env var is set — it instead prints the skills and the exact //path (or --skill/--all) commands to script the choice, and exits non-zero, so scripts and CI never hang on a prompt.
  • A pasted GitHub browser URL (https://github.com/owner/repo/tree/<ref>/<path>, or a /blob/ link to a file such as SKILL.md, which maps to the file's directory) is normalized to the canonical repo//path@ref form at add time. The ref is taken to be the single segment after /tree/; for a ref that itself contains /, spell the source out as repo//path@ref. Symlinks inside a fetched source are recreated as symlinks, never followed (the content hash covers the link target string, not what it points at).
  • sync installs exactly what the lock pins and resolves+locks manifest entries that are not locked yet. It never re-resolves already-locked versions. Skills with local modifications (drift) are warned about and skipped with a non-zero exit; --force restores them. Entries removed from the manifest are pruned (drifted ones only with --force).
  • update is the only command that moves already-locked versions: it re-resolves each entry's ref (or default branch) to the current commit, re-materializes and re-links it, and rewrites the lock. With no argument it updates every skill; with a name, only that one. Editable entries have nothing to re-resolve and are skipped; drifted skills are skipped with a non-zero exit unless --force overwrites them.
  • remove drops a skill from the manifest and lock, unlinks it from every harness, and deletes its materialized copy. An editable skill loses only its canonical link — the working tree is left untouched. A drifted skill is refused unless --force, since removal discards local edits.
  • list shows each managed skill with its pinned commit (or editable) and status (ok, drifted, missing, not-locked). Skills still in the lock but removed from the manifest show pruned on next sync; skills found in the skills dir or a harness dir but absent from the manifest show unmanaged. It only observes.
  • harness controls which harnesses get per-skill links, via a harnesses key in the manifest. The effective set is the union of the project manifest's key and the machine manifest's, so personal harnesses apply everywhere without touching a shared skiletto.toml. enable links every installed skill immediately; disable unlinks (warning when the harness stays enabled machine-wide); sync keeps links reconciled with the key. A scope with no key anywhere gets a one-time picker on the first interactive add/sync/import (the answer is saved); without a TTY the command installs to the canonical dir only and prints a note — never an error, since canonical-only is always safe. If a harness link path already resolves to the canonical directory (say .claude/skills is your own symlink to .agents/skills), skiletto treats it as linked and touches nothing.
  • import bootstraps skiletto.toml and skiletto.lock from a Vercel npx skills skills-lock.json (default: one in the current directory). Only the current version 3 lock format is read; older versions are rejected, since Vercel wipes them and starts fresh, so none survive on disk. The lock records skillPath as the SKILL.md file, whose directory import recovers; a repo-root skill becomes path ., which pins the source root itself even when the repo also contains nested skills. It maps each entry to a canonical git source (github and git sourceTypes; local and well-known entries are reported with guidance instead), resolves the default-branch HEAD to a commit — Vercel's lock stores no ref or SHA, so HEAD is the best available pin — and installs and links each skill. Entries already in the manifest are skipped; entries that cannot be mapped or resolved are reported and cause a non-zero exit without aborting the ones that do resolve. Import is one-way. --global writes the machine scope. Installed trees that import cannot prove pristine (drifted lock orphans, unmanaged skill dirs) are refused unless --force replaces them. Migrating: real skill directories that npx skills left in .claude/skills/ are never touched — import refuses each one and names it; remove the old copy (rm -r .claude/skills/<name>) and re-run import.
  • --editable (local paths only) symlinks the working tree instead of copying a pinned commit, so edits are live; such entries carry no commit/hash and are never drift-checked.
  • --global / -g (on any command) switches to the machine scope: the manifest and lock live in the platform config dir (~/.config/skiletto/ on Linux; XDG_CONFIG_HOME is honored on every platform). Setting SKILETTO_CONFIG_DIR overrides both: it names the directory holding the machine-scope manifest and lock directly, no skiletto/ subdirectory appended. Skills materialize in ~/.agents/skills/, and the Claude adapter links into ~/.claude/skills/. Local path and editable sources are the normal case here, so add skips the portability warning. The machine scope is always explicit: running in your home directory without --global is an error, not a project rooted at ~ — a home-rooted "project" would silently share ~/.agents/skills with the machine scope.
  • --no-input (on any command) forces the non-interactive path: instead of prompting, skiletto fails with an actionable error listing the flags that script the choice. A set CI env var implies it.
  • --verbose / -v (on any command) prints extra diagnostics to stderr, including a line for each pre-install hook run naming the skill and the event. Because -v means verbose, --version has no shorthand.

Pre-install hook

skiletto can run a security scanner — or any command — over a skill's content before it is installed. Configure it under [hooks] in the machine-scope manifest (~/.config/skiletto/skiletto.toml on Linux):

[hooks]
pre-install = 'skillspector scan --no-llm "$SKILETTO_SKILL_DIR"'
  • The command runs after the skill's content is fetched into a staging directory and before anything is installed or locked. The staged directory is exported as SKILETTO_SKILL_DIR — reference it in the command ("$SKILETTO_SKILL_DIR"; %SKILETTO_SKILL_DIR% on Windows) — alongside SKILETTO_SKILL_NAME, SKILETTO_SOURCE, SKILETTO_COMMIT, and SKILETTO_EVENT (add, update, sync, or import). Exit 0 lets the install proceed; any other exit aborts it with nothing changed — on disk, in the manifest, or in the lock.
  • The hook gates content entering the lock: it runs on add, update, import, and on sync only for manifest entries that are not locked yet. Re-installing already-locked content (sync materializing from the lock) skips it — the lock's content hash guarantees those are byte-for-byte the contents that were scanned when the entry was locked. Editable skills are never scanned: their working tree changes after any scan.
  • Hooks run only from the machine manifest, and it applies in every project. A [hooks] table in a project's skiletto.toml is ignored with a warning: hooks execute arbitrary commands, so a cloned repository must not be able to supply one — or to replace your scanner. --no-hooks (on add/sync/update/import) skips the hook for one run.
  • The gate fails closed: an unreadable machine manifest or an unknown name under [hooks] makes installs fail rather than silently skipping the hook.
  • The command runs through sh (cmd.exe on Windows), so it can carry its own flags and environment variables.
Example: scanning skills with SkillSpector

SkillSpector is NVIDIA's security scanner for agent skills. Its exit codes fit the hook contract directly: 0 (safe or caution), 1 (do not install), 2 (scan error) — so a risky skill blocks the install. Install it once with uv:

uv tool install git+https://github.com/NVIDIA/skillspector.git

The [hooks] entry above runs its static analysis: fast, offline, and the skill's contents never leave your machine. For the full scan with LLM semantic analysis, use the claude_cli provider — it drives your locally installed, already-authenticated claude CLI, so no API key is configured anywhere:

[hooks]
pre-install = 'SKILLSPECTOR_PROVIDER=claude_cli skillspector scan "$SKILETTO_SKILL_DIR"'

Note that LLM analysis sends the scanned skill's file contents to the model provider; keep --no-llm if they must stay local.

Windows

Linking a skill into a harness dir tries three strategies in order:

  1. symlink — works when Developer Mode is on (or the shell is elevated);
  2. directory junction — the normal Windows path, no privilege required;
  3. copy — a last resort when neither reparse point can be created.

skiletto records nothing about which strategy it used: symlinks and junctions identify themselves, and a copy is recognized as skiletto's own only when its contents still hash-match the canonical .agents/skills/<name> tree. A copied link that has diverged is treated like any other local modification — sync, update, and remove refuse to overwrite or delete it without --force.

Two consequences of the copy fallback:

  • A pristine copy — one that still matches the canonical tree — behaves exactly like a symlink: sync re-links over it, update refreshes it in place, remove deletes it, none of them need --force. Only a copy you have edited by hand is refused without --force.
  • Editable installs need a symlink or a junction — a copy cannot stay live. On a filesystem where only copying works, skiletto add --editable fails with a clear message instead of silently installing a stale snapshot.

On Linux and macOS the behavior is unchanged: skiletto symlinks, or fails.

Development

Requires Go 1.26+ and system git.

lefthook install   # gofmt + golangci-lint on commit, tests on push
go test ./...
go build .

License

MIT

Documentation

The Go Gopher

There is no documentation for this package.

Directories

Path Synopsis
internal
adapter
Package adapter defines the harness Adapter extension point and a compiled-in registry.
Package adapter defines the harness Adapter extension point and a compiled-in registry.
adapter/harness
Package harness holds the compiled-in harness adapters.
Package harness holds the compiled-in harness adapters.
cli
Package cli wires the skiletto command-line interface.
Package cli wires the skiletto command-line interface.
engine
Package engine orchestrates skiletto: it diffs manifest, lockfile, and disk state into a Plan of actions, then applies them through sources and adapters.
Package engine orchestrates skiletto: it diffs manifest, lockfile, and disk state into a Plan of actions, then applies them through sources and adapters.
gitcli
Package gitcli wraps the system git binary.
Package gitcli wraps the system git binary.
lockfile
Package lockfile reads and writes skiletto.lock, the generated file pinning every installed skill to an exact commit and content hash.
Package lockfile reads and writes skiletto.lock, the generated file pinning every installed skill to an exact commit and content hash.
manifest
Package manifest reads and writes skiletto.toml, the hand-editable file recording which skills the user wants installed.
Package manifest reads and writes skiletto.toml, the hand-editable file recording which skills the user wants installed.
scope
Package scope resolves where a scope keeps its manifest, lockfile, and canonical skills directory.
Package scope resolves where a scope keeps its manifest, lockfile, and canonical skills directory.
skill
Package skill locates skill directories (any directory containing SKILL.md), derives default skill names, and hashes installed trees for drift detection.
Package skill locates skill directories (any directory containing SKILL.md), derives default skill names, and hashes installed trees for drift detection.
source
Package source defines the Source extension point: something that can resolve a ref to a commit and fetch a subtree at that commit.
Package source defines the Source extension point: something that can resolve a ref to a commit and fetch a subtree at that commit.
ui
Package ui owns interactive prompting.
Package ui owns interactive prompting.
vercelimport
Package vercelimport reads Vercel's skills-lock.json and maps its entries to canonical git sources.
Package vercelimport reads Vercel's skills-lock.json and maps its entries to canonical git sources.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL