@ray_of_goodness_/skillslink
v0.4.3
Published
Find, validate, publish and pull agent skills and Agent Plugins from a SkillsLink registry
Maintainers
Readme
@ray_of_goodness_/skillslink
Find, validate, publish and pull agent skills and Agent Plugins
packages from the terminal. No runtime dependencies. Installs a skillslink command.
Install
npm i -g @ray_of_goodness_/skillslink
# or run it without installing
npx @ray_of_goodness_/skillslink scanWorking on the CLI itself? Link your checkout instead:
cd packages/cli && npm linkCommands
skillslink login [--registry <url>] Store an API token in ~/.skillslink.json
skillslink logout Remove the stored token
skillslink whoami Show the current user and registry
skillslink scan [dirs...] [--all] [--depth <n>] [--json] [--push]
skillslink plugins [dirs...] [--depth <n>] [--json] [--push] [--fix]
skillslink validate [dir] [--json] [--fix] [--dry-run] [--yes]
skillslink push <dir> [--slug <slug>] [--version <v>] [--public]
skillslink sync [--prefix <p>] [--dry-run] [--public] [--yes]
skillslink pull <author/slug> [--dir <target>] [--version <v>]
skillslink install [<author/slug>...] [--all] [--force] [--dry-run] [--yes]
skillslink backups Snapshots taken before each install
skillslink restore [<id>] [--prune] [--dry-run] [--yes]
skillslink list Everything you published
skillslink delete <slug...> [--dry-run] [--yes]
skillslink config Show current config (token masked)
sync options
--prefix <p> Prefix every slug, not just the conflicting ones
--dry-run Show the plan without uploading
--public Publish everything publicly (private by default)
--yes Skip the confirmation prompt
--skills-only Skip configs and plugins
--configs-only Skip skills and plugins
--plugins-only Skip skills and configs
--no-plugins Skip plugins
install options
--all Install everything you published
--dir <root> Install under this root instead of the published source
--force Overwrite local files that differ
--dry-run Show the plan without writing
--yes Skip the confirmation prompt
--no-backup Skip the pre-install snapshot
--skills-only Skip configs and plugins (with --all)
--configs-only Skip skills and plugins (with --all)
--plugins-only Skip skills and configs (with --all)
--no-plugins Skip plugins (with --all)
plugin options
--fix Migrate a pre-standard package to Agent Plugins 1.0.0
--dry-run Show the migration without writing
--yes Skip the confirmation prompt
--no-backup Skip the pre-migration snapshot
--push Publish every valid plugin found
--json Machine-readable report
restore options
--prune Also delete files the install created
--dry-run Show what would be restored
--yes Skip the confirmation prompt
delete options
--dry-run Show what would be deleted
--yes Skip the confirmation promptScanning
scan walks directories and reports every folder containing a SKILL.md, reading name and
version from the frontmatter. It stops descending once it finds a SKILL.md, follows symlinks
without looping, and skips node_modules, VCS folders, build output and Library.
Default roots:
~/.claude/skills ~/.claude/plugins ~/.config/claude/skills
~/.codex/skills ./.claude/skills ./skills ./examplesPass directories explicitly to override them, or --all to sweep the whole home directory
(depth 8). --push publishes everything found; skills whose version already exists are reported
as skipped rather than failing the run.
Agent Plugins
An Agent Plugins package is a directory with a
plugin.json manifest. It ships skills under skills/, MCP servers in mcp.json and
client-specific files under reverse-domain directories, and it travels as one unit.
skillslink validate ./my-plugin # conformance report, exit code 1 when invalid
skillslink plugins # every plugin.json on the machine
skillslink push ./my-plugin # publish the package
skillslink install bohdan/my-plugin # back to ~/.claude/plugins/<manifest name>validate implements Agent Plugins 1.0.0 rather than just its JSON Schemas, so it enforces what
the schemas cannot express:
plugin.jsonis a closed document. Unknown top-level fields and a non-objectextensionsare reported and ignored; anything else invalid — a badname, a foreignauthorfield, an unsupported$schema— rejects the package.nameis 1–64 characters ofa-z0-9.-, alphanumeric at both ends, with no--or...mcp.jsonis a closed union per transport. Acommandmust be one executable token — a bare name or a./path inside the package, never a shell string.cwdmust be./…,${PLUGIN_ROOT}…or${PLUGIN_DATA}…and may not escape what it is rooted in. Remoteurls must be absolute HTTPS (HTTP only for loopback), with no user information and no fragment.envmay not setPLUGIN_ROOTorPLUGIN_DATA— the loading client provides those.- Only immediate children of
skills/holding aSKILL.mdare skills; deeper directories are never searched. - Failures stay at their own boundary: a broken
mcp.jsondisables MCP, a single invalid server entry is skipped, and neither stops the rest of the package from loading.
push picks the kind from the directory — a plugin.json means a plugin, a bare SKILL.md means
a skill — and the registry re-runs the same validation before storing anything.
Installed plugins land in ~/.claude/plugins/<manifest name>, one directory rooted at a single
location. PLUGIN_ROOT and PLUGIN_DATA are the loading client's job, not the installer's.
Migrating a pre-standard package
Plugins written before the specification keep their manifest under .claude-plugin/,
.codex-plugin/ and the like, with client-specific fields at the top level and MCP servers either
inline or in a .mcp.json of their own. --fix turns one into a conformant package:
skillslink validate ./old-plugin --fix --dry-run # what it would write, and why
skillslink validate ./old-plugin --fix # asks, snapshots, then writes
skillslink plugins --fix --dry-run # every non-conformant package on the machine
skillslink plugins --fix # migrate them in one passWhat it does:
- Writes a root
plugin.jsonwith the canonical$schema, keepingname,version,description,author,homepage,repository,licenseandkeywordswhere they are legal. - Moves everything the closed schema has no room for —
commands,hooks,displayName,interface, a strayauthor.github, aversionthat is a number — underextensions["com.anthropic.claude-code"](or the namespace matching the vendor directory it came from). Nothing is dropped. - Normalises a name that breaks the rules:
My Awful--Name!!becomesmy-awful-name, and the run says so. - Collects MCP servers from the inline
mcpServers,./.mcp.jsonand.claude-plugin/.mcp.json— including the bare-map variant with nomcpServerswrapper — into a propermcp.json: transport inferred fromcommandorurl,${CLAUDE_PLUGIN_ROOT}renamed to${PLUGIN_ROOT}, a root-anchoredcommandrewritten as a./path, a barecwdrewritten as./…, and aPLUGIN_ROOTorPLUGIN_DATAentry dropped fromenvsince the client supplies it.
What it refuses to guess is reported as manual and left for you: a command that is a shell
string (node server.js --flag), a plain-HTTP endpoint on a non-loopback host. Those servers are
skipped rather than written half-right.
The legacy manifest stays where it is. It is what the old client still reads, so a migrated
package works with both. Every file is snapshotted before it is written, so
skillslink restore <id> --prune undoes the whole migration.
Syncing everything
sync pushes your skills and your agent configs in one run.
skillslink sync --dry-run # show the plan, upload nothing
skillslink sync # asks for confirmation, then pushesIt collects agents, commands, hooks, prompts, rules, output-styles and workflows,
plus the loose *.json / *.toml / *.yaml settings at the root of each agent directory:
~/.claude ~/.codex ~/.cursor ~/.gemini ~/.opencodesync also picks up plugin packages from ~/.claude/plugins and its siblings, and a skill that
lives inside one is not published separately — the package already carries it. Invalid packages are
skipped; run skillslink validate on them to see why.
Left out of the config bundles on purpose: skills and plugins (they are published as skills
and plugins instead), per-machine runtime state (projects, todos, history, logs, cache,
shell-snapshots), and anything matching *.local.*, credentials or auth.
Secrets are redacted before upload. Values under key names like token, api_key, secret
or password, every entry inside an env or headers map, and anything shaped like a known
credential (sk-ant-…, ghp_…, xoxb-…, JWTs) are replaced with __REDACTED__. The run reports
how many values it scrubbed. Re-enter them by hand after pulling.
Everything you push is private unless you pass --public.
Name conflicts
Two sources often use the same folder name — ~/.claude/agents and ~/.cursor/agents both want
the slug agents. The colliding ones get their source as a prefix; unique names are left alone:
claude/agents -> claude-agents
cursor/agents -> cursor-agents
claude/commands -> commandsSkills that ship with a plugin are namespaced by that plugin instead, so two plugins can each
carry an access skill, and a slug never moves the day you write a skill of the same name:
plugins/…/telegram/skills/access -> telegram-access
plugins/…/discord/skills/access -> discord-access
~/.claude/skills/access -> accessA plugin also keeps its skills in several places at once — a marketplace checkout plus one cache
directory per version ever installed. Those are the same skill, so a scan reports the checkout
(or the newest cached version) and ignores the rest instead of publishing access-2, access-3.
The plugin name comes from its .claude-plugin/plugin.json, not from the directory it happens to
sit in — a checkout under marketplaces/marketingskills and its cache copy under
cache/marketingskills/marketing-skills both resolve to marketing-skills, so uninstalling the
checkout does not rename anything you already published. Without a manifest the directory name is
used instead.
--prefix work applies on top of all that, giving work-claude-agents and work-commands. A
slug already taken in the registry by a different source is prefixed the same way.
Re-running sync is cheap: an unchanged bundle is reported as same and stores nothing, while a
changed one is published as the next patch version.
Setting up a second machine
install is the reverse of sync. On a fresh machine, log in and pull everything back down:
skillslink login
skillslink install --all --dry-run # show where every bundle would land
skillslink install --all # asks for confirmation, then writesEach bundle goes back where it came from. Skills land in ~/.claude/skills/<slug>; configs
return to the directory they were published from, so a codex-settings bundle goes to ~/.codex
and a settings bundle to the root of ~/.claude rather than into a folder of that name.
--dir <root> overrides the root for the whole run.
A single bundle works too, and needs no token when it is public:
skillslink install bohdan/code-reviewLocal files win. A file that already exists with different contents is reported as kept and
left alone — --force overwrites it. Unchanged files are same and are not rewritten, so
re-running install is a no-op. Bundle paths that would escape their target directory are
refused outright.
Configs come back with __REDACTED__ where your secrets were; the run tells you how many files
need them filled in again.
Backups and rollback
Every install snapshots each file it is about to write before writing it, into
~/.skillslink/backups/<id>/. The last 20 runs are kept.
skillslink backups # ids, newest first
skillslink restore # put the newest snapshot back
skillslink restore <id> --dry-run # see exactly what would change
skillslink restore <id> --prune # also delete the files that install createdFiles that existed are restored from the snapshot. Files the install created are recorded too but
left in place unless you pass --prune, which is what makes a rollback complete. --no-backup
skips the snapshot for that run.
sync needs no backup — it only reads local files and uploads them.
Deleting
delete removes your own skills and configs from the registry, every version of them, for good.
Slugs accept * and ?:
skillslink delete my-skill # asks for confirmation
skillslink delete '*-2' '*-3' --dry-run # preview a cleanup sweep
skillslink delete '*-2' --yes # and run itauthor/slug works too, as long as the author is you. Without a terminal to answer the prompt,
--yes is required.
Configuration
| Source | Key |
| --- | --- |
| ~/.skillslink.json | registry, token |
| Environment | SKILLSLINK_REGISTRY, SKILLSLINK_TOKEN |
| Flags | --registry, --token |
Flags win over environment, environment wins over the config file. Default registry is
https://skills-sync-web.vercel.app.
Bundle rules
A skill directory must contain a SKILL.md. Dotfiles, node_modules, .git, dist, .output
and .nuxt are skipped. Text files only: max 200 files, 1 MB per file, 8 MB total. --slug and
--version fall back to name and version in the SKILL.md frontmatter.
A plugin directory must contain a plugin.json. Its whole tree is bundled — dotfiles included,
since client extension data lives there — minus version control and build output. Max 300 files,
1 MB per file, 8 MB total, and --slug and --version fall back to name and version in the
manifest. Binary files cannot be published: bundles are stored as text, so a plugin must not ship
a compiled executable.
