skillshelf
v0.7.1
Published
Agent-first skill registry + manager for Claude Code and compatible agents.
Maintainers
Readme
skillshelf
A package manager for your agent skills — one canonical library, loaded on demand, never all at once.
Your skills are scattered across every agent you use — ~/.claude/skills, ~/.codex/skills,
~/.cursor/skills, Obsidian vaults, and a dozen per-project .claude dirs. Copies drift, you
rewrite skills you already have, and you forget which exist. The naive fix — dump everything into
one agent's dir — makes every session pay to load hundreds of skill descriptions at once.
skillshelf is the middle path: one git-backed library that is a passive shelf (nothing
auto-loads), plus a CLI to search, tag, bundle, and load exactly the skills a project needs,
when it needs them. It's agent-agnostic (Claude Code, Codex, Cursor, …) — skl where maps
where every skill is actually deployed across all of them, surfacing untracked copies, drift, and
dead links. Find anything in one place; pay only for what you use. (Complementary to installers
like vercel-labs/skills.)
Desktop app
A cross-platform desktop UI (React + Tauri) sits on top of the same engine — a Library-first
workbench for managing where every skill is deployed across your agents. It reads the real
skl library (no separate data store) and every toggle writes the same symlinks the CLI does.

- Library-first, skill-centric list — scan all skills, flip them on/off per agent inline; a top scope switcher (Global · each project · + Add project) and a per-scope count bar.
- Project scopes — manage a project's loadout without leaving the app; globally-deployed skills show an "active via Global" inherited state so you always know what's effectively live.
- Two-tier toggles — a clean on/off for the happy path, but drift / copy / dead / aliased surface a resolve flow instead of silently doing the wrong thing (state is derived from the filesystem, never stored — so the UI can't lie about what's deployed).

The detail drawer shows a skill's body, tags, provenance, and an agent × scope deployment
matrix (Global + the projects where it's pinned), plus lifecycle actions.
Browser/dev mode (
cd app && bun run dev) renders synthetic fixtures with no backend; the packaged desktop app talks to your realsklengine. Seedocs/adr/0010-*for the design.
Install
skillshelf runs on Bun (>= 1.2). No other runtime dependencies.
Bun is required, not optional. The
sklbin is a TypeScript entrypoint with a#!/usr/bin/env bunshebang — there is no compiled Node build.npm i -g skillshelfwill not give you a workingskl(apreinstallguard aborts with a pointer to Bun); usebunxorbun add -ginstead.
# Run it without installing
bunx skillshelf <command>
# Or install the `skl` binary globally
bun add -g skillshelf
skl <command>Quickstart
# 1. Set up the config + library and link the thin global core
skl init
# 2. Scaffold a new skill into the library (or `skl add` a third-party one)
skl new rnaseq-qc --domain bioinfo --desc "QC gate for RNA-seq count matrices"
# 3. Find skills across the whole library
skl ls
skl search rnaseq
# 4. Read a skill's instructions on demand — no token cost until you ask
skl show rnaseq-qc
# 5. Activate a domain bundle in the project you're working in
cd ~/projects/my-analysis
skl use bioinfo # symlinks every skill tagged `bioinfo` into ./.claude/skills
skl status # what's currently linked here
skl drop bioinfo # unlink when you're doneAdd --json to any command for machine-readable output (skillshelf is built to be driven
by an agent as well as a human).
Migrating scattered skills
Already have skills strewn across ~/.claude/skills, an Obsidian vault, and a dozen project
.claude dirs? Consolidate them with three deterministic primitives — scan discovers,
import adopts, infer tags. The judgment in between (which copies to keep, which drift wins)
is yours (or your agent's); the tool never guesses.
# 1. Register the places your skills live, then take a read-only inventory.
# scan moves nothing — it just reports candidates, duplicates, and drift.
skl scan --add-root ~/.claude/skills
skl scan --add-root ~/notes/.agents/skills
skl scan # report every candidate + duplicate/drift group
# 2. Adopt the ones you want, one at a time. Each import moves the skill into the
# library and leaves a symlink behind so old paths keep resolving.
skl import rnaseq-qc --from ~/.claude/skills/rnaseq-qc
skl import headline-picker --from ~/notes/.agents/skills/headline-picker
# For a skill living inside a project repo, copy instead of move (no symlink left behind):
skl import deploy-check --from ~/projects/web/.claude/skills/deploy-check --copy
# When two copies drifted and you've picked the winner, overwrite the loser:
skl import rnaseq-qc --from ~/projects/lab/.claude/skills/rnaseq-qc --force
# For a skill you actively develop in its own git repo, shelve a LINK instead of a copy —
# the repo stays canonical and edits show up live, no drift, no re-sync (ADR-0004):
skl link --from ~/dev/headline-picker/skill/headline-picker
# 3. Tag the now-populated library in one pass. Domain is tags, not folders, so this
# runs AFTER import with no reorg — no skill ever has to move because a tag changed.
skl infer --emit # hand the payload to your agent, then `skl infer --apply`Domain lives entirely in tags (ADR-0001): the
library layout is flat (library/<name>/) and import never decides a domain, so there is no
chicken-and-egg between adopting a skill and tagging it.
How it works
skillshelf separates owning a skill from loading it.
- Canonical library — a dedicated git repo, one copy per skill in a flat, non-semantic
layout (
library/<name>/). This is a passive shelf: nothing here auto-loads, which is exactly what kills the all-at-once token cost. - Domain bundles — domain is tags, not folders (ADR-0001).
A skill tagged
domains: [coding, bioinfo]shows up in both bundles from a single copy on disk;skl use bioinforesolves every skill carrying that tag.primaryDomainis justdomains[0], never inferred from a folder. - Thin global core — a handful of universal skills (commit, search, memory) are
symlinked permanently into
~/.claude/skillsso they always auto-trigger. Small, bounded token cost — "some loaded is fine; all-at-once is the problem." - On-demand
show— prints only the SKILL.md instruction body and lists the paths of any bundled reference files (without reading them). Progressive disclosure: cheap by default, deep when you ask. Works mid-task with no reload. - Owned vs linked entries (ADR-0004) — an entry
either owns its bytes (a real copy — for downloads and stabilized skills) or is linked
(
skl link --from <dev-repo>— a symlink to an external repo that stays canonical, for skills you develop in their own git).update/outdatedskip linked entries so they never push upstream into your dev repo. The mode is derived from the filesystem, never stored, so it can't go stale. - Updates never clobber your tags — domain tags live in the central
taxonomy.json(ADR-0002), separate from the skill body, soskl updatecan swap an owned skill's upstreamSKILL.mdcleanly while your taxonomy survives.
flowchart LR
L["📚 Canonical library<br/><i>passive git shelf</i>"]
B["🏷️ Bundles<br/><i>tag query — bioinfo · coding · …</i>"]
P["📁 Project .claude/skills/<br/><i>symlinks, on demand</i>"]
G["⚡ ~/.claude/skills<br/><i>thin global core, always-on</i>"]
L -- "skl use bundle" --> B
B -- symlink --> P
L -- "thin global core" --> G
search(["skl search · ls · show"]) -. reads .-> L
classDef shelf fill:#1f2937,stroke:#4b5563,color:#e5e7eb;
classDef live fill:#064e3b,stroke:#10b981,color:#d1fae5;
class L,B shelf;
class P,G live;See docs/ARCHITECTURE.md for the full design.
Command reference
| Command | Summary | Key flags |
|---|---|---|
| skl init | Set up ~/.skillshelf config + library and link the global-core skills | --force |
| skl scan [roots…] | Read-only discovery of skill candidates across roots (counts, duplicates, drift) | --add-root <path>, --remove-root <path> |
| skl roots | List the persisted scan roots (read-only; no crawl) | — |
| skl import <name> --from <path> | Adopt your own skill into the library as an OWNED copy (move + symlink-back, or --copy) | --copy, --as <slug>, --force |
| skl link [<name>] --from <dev-repo> | Shelve a dev-repo skill as a LINKED entry (library symlinks to it; the repo stays canonical). --at <path> instead collapses a stray copy into the library | --from, --at, --force |
| skl new <name> | Scaffold a new skill dir + SKILL.md into the library | --domain <d>, --desc "...", --force |
| skl ls [bundle] | One-line listing of the library, or one bundle (--json carries mode/linkTarget) | --all, --sort modified\|name\|domain\|deploys\|source |
| skl search <kw...> | Fuzzy match over name + description + domains across the library | — |
| skl show <name> | Print a skill's SKILL.md body; list reference-file paths (not contents) | — |
| skl tag <name> <domain>… | Add domain tag(s) to a skill in the central taxonomy (deterministic, no LLM) | — |
| skl untag <name> <domain> | Remove a domain tag from a skill | — |
| skl retag <old> <new> | Rename a domain across the whole library taxonomy (deterministic) | — |
| skl rename <old> <new> | Rename a skill slug atomically (dir + frontmatter + taxonomy + lock). Alias skl mv | — |
| skl retire <name…> | Soft-delete skill(s) into _retired/ (reversible; excluded from deploys) | — |
| skl unretire <name…> | Restore retired skill(s) back to the active library | — |
| skl rm <name…> | Delete skill(s) (dir/symlink + taxonomy + lock), re-index. Refuses a live OWNED skill without --force; a LINKED entry rms freely (safe unlink) | --force, --dry-run |
| skl status | Show which library skills are linked into an agent's project skills dir (default ./.claude/skills); flags unmanaged real copies (drift-prone) | --agent <id>, --project <dir> |
| skl where [name] | Map where each skill is deployed across all agents (Claude, Codex, Cursor…); flags copies, drift, 2nd-sources, dead links — a dev repo a library entry links to shows as a clean ✓ source | --problems, --prune, --fix, --dry-run |
| skl agents [name] | Per-agent × per-scope deployment matrix (one skill's row, or the whole library) | --agent <id>, --project <dir> |
| skl use <bundle\|skill>… | Symlink bundle(s)/skill(s) into an agent's skills dir (default ./.claude/skills/, hot-loads). --force replaces a real-file conflict with the library symlink | --agent <id>, --global \| --project <name>, --force |
| skl drop <bundle\|skill>… | Remove bundle/skill symlinks from an agent's skills dir | --agent <id>, --global \| --project <name> |
| skl realign <deployed-name> | Rename an aliased deployment symlink (wrong name) to match its library skill | --agent <id>, --global \| --project <name> |
| skl diff <name> | Unified diff of a deployed copy's SKILL.md against the library skill (read-only) | --agent <id>, --global \| --project <name> |
| skl refresh | Re-sync this project's ./.claude/skills symlinks to current library reality (repoint stale, prune vanished) | --dry-run |
| skl projects | Manage the persisted nav projects shown as scopes in the desktop app (ls/add/rm) | — |
| skl add <src> | Install third-party skill(s) into the library (librarian only — no agent-dir writes). One repo = one clone: a bare repo with several skills needs --all/--skill/--list; single-skill add <repo>/<path> is unchanged. --all installs the published set (the .claude-plugin/marketplace.json manifest allowlist when present, else every discovered skill; minus metadata.internal); an unpublished skill installs only via --skill <name>. A published set over 15 skills refuses without --yes (a count gate on blast radius; --skill is never gated). --list discovers + prints (marks published/unpublished); --dry-run previews drift (new/identical/differs); a differs skill is skipped without --force (ADR-0012) | --all, --skill <a,b>, --list, --yes, --dry-run, --domain <d>, --name <slug>, --no-infer, --force |
| skl track <name> --source <src> | Adopt provenance for a library skill you already have (offline; no re-download). skl untrack drops the lock entry (idempotent inverse) | --ref <r>, --resolve, --force |
| skl migrate | Bulk-adopt provenance from a vendor skill-lock for skills already in your library | --from <path>, --dry-run, --resolve, --force |
| skl outdated [name] | Check upstream ref per tracked skill and mark stale ones (LINKED dev-repo entries are reported, never probed); --check-local diffs the local body against its baseline offline | --check-local |
| skl update [name] | Re-pull upstream body, preserve domain tags, diff if local body diverged (LINKED entries are skipped — their own git owns versioning) | --force, --dry-run |
| skl index | Regenerate INDEX.md (catalog grouped by domain) | — |
| skl infer | Re-run AI domain taxonomy over the library (emit/apply/provider modes) | see below |
Every command also accepts --json. Destructive/edit verbs (rm, retire/unretire,
rename, realign, tag/untag/retag, scan --remove-root, where --prune/--fix,
refresh) are the inverse + fine-grained-edit family from
ADR-0005: reversible by default, transactional
across the skill dir + taxonomy.json + shelf.lock.json + INDEX.md.
AI taxonomy & inference
Domains and tags can be inferred and re-run by an LLM via skl infer. This is optional —
the entire core (search, ls, show, use, bundles, add, update) works fully without any LLM.
Inference has three mutually exclusive modes:
skl infer [--emit | --apply <file.json> | --provider <name>] \
[--base-url <url>] [--model <id>] [--include-retired] [--json]Agent modes (no network call from skillshelf):
--emit— print a self-contained prompt + the library payload as JSON. Hand it to whatever agent or model you already have open; it does the reasoning.--apply <file.json>— apply the taxonomy proposal the agent produced back into the library (for review/approval), writing tags into the centraltaxonomy.json.
API mode (skillshelf calls an OpenAI-compatible endpoint itself):
Entered when either --provider or --base-url is given. The request is
POST {base}/chat/completions, OpenAI schema, temperature: 0,
response_format: {type: "json_object"} (strict JSON; falls back to brace-extraction if the
model wraps JSON in prose or fences).
--provider <name> is sugar that only sets a default base URL — the API key always comes
from the environment or a dotenv file.
| Provider | Base URL |
|---|---|
| openai | https://api.openai.com/v1 |
| openrouter | https://openrouter.ai/api/v1 |
| groq | https://api.groq.com/openai/v1 |
| ollama | http://localhost:11434/v1 |
| custom | resolved entirely from --base-url / env |
Resolution order, applied independently to base URL, API key, and model (highest precedence first):
- CLI flags (
--base-url,--model;--providerfor the base-URL preset) - Environment variables —
SKILLSHELF_LLM_*primary, thenOPENAI_*fallback - Optional dotenv file at
$SKILLSHELF_ENV_FILE(default:./.envif it exists, else none)
Environment variables:
| Variable | Purpose |
|---|---|
| SKILLSHELF_LLM_BASE_URL | base URL including /v1 |
| SKILLSHELF_LLM_API_KEY | bearer API key |
| SKILLSHELF_LLM_MODEL | chat model id |
| SKILLSHELF_ENV_FILE | path to a dotenv file (optional; default ./.env if present) |
| OPENAI_BASE_URL / OPENAI_API_KEY / OPENAI_MODEL | convention fallbacks for the three above |
Defaults: base URL https://api.openai.com/v1, model gpt-4o-mini (a placeholder —
override with --model or *_MODEL).
The dotenv parser supports KEY=value and export KEY=value, strips surrounding quotes,
ignores blank and # comment lines, and never throws.
Errors are deterministic and raised before any network call:
- No resolvable key →
missing API key. Set SKILLSHELF_LLM_API_KEY (or OPENAI_API_KEY) in the environment or a dotenv file ($SKILLSHELF_ENV_FILE, default ./.env). - Unknown provider →
unknown provider "X". known: openai, openrouter, groq, ollama, custom
Example:
export SKILLSHELF_LLM_API_KEY=sk-...
skl infer --provider openai --model gpt-4o-mini
# or fully self-hosted:
skl infer --base-url http://localhost:11434/v1 --model llama3.1Configuration
| Setting | What it controls | Default |
|---|---|---|
| SKILLSHELF_LIBRARY | path to the canonical library (env, highest precedence) | ~/.skillshelf/library |
| ~/.skillshelf/config.json | { "library": "...", "globalCore": "..." } | — |
| SKILLSHELF_GLOBAL_CORE | where global-core skills are symlinked | ~/.claude/skills |
Library path resolution: SKILLSHELF_LIBRARY → config.json → default. See the
AI taxonomy section for the SKILLSHELF_LLM_* / OPENAI_* /
SKILLSHELF_ENV_FILE inference variables.
Contributing
Contributions are welcome — see CONTRIBUTING.md. Tests run on Bun:
bun testLicense
MIT © skillshelf contributors.
