Repository navigation
Description
Why
Many libraries are published as npm packages. A library author who writes a skill for their library has no good way to ship it with the package today. Users must find the skill in a Git repository and run skills add. When the library updates, the skill does not.
skills-npm solves this as a separate tool. It imports the internals of this CLI through a Git submodule. This RFC proposes to enhance skills experimental_sync by porting the features of skills-npm into it, in small steps that each ship on their own. When the port is complete, the command graduates to skills sync.
Examples
To demonstrate the idea better, there are a few example scenarios:
A library ships its skill with the package
agent-browser is an npm package and has a skill in the same repository. Today a user installs the package and the skill in two unrelated steps:
pnpm add agent-browser
npx skills add vercel-labs/agent-browserThe two can drift apart. The package updates with pnpm update, the skill does not.
With this RFC, the author has two options.
Option A: ship the skills/ folder inside the npm package. agent-browser already does this (skills is in its files list). The user adds one script:
{
"private": true,
"scripts": {
"prepare": "skills experimental_sync" // <---
}
}After pnpm install, .agents/skills/agent-browser is a symlink into node_modules/agent-browser/skills/agent-browser. The skill version always matches the installed package version. The link is committed, so every clone gets the same skill.
Option B: keep the skill in the Git repository and declare it in package.json of the npm package:
{
"name": "agent-browser",
"files": ["dist"],
"version": "1.0.0",
// ...
"skills": ["vercel-labs/agent-browser"] // <--
}skills experimental_sync sees that agent-browser is an installed dependency, reads its skills field, and runs the equivalent of npx skills add vercel-labs/agent-browser. The user does not need to know that the skill exists.
A skills pack
The skills field also makes a package that ships no code and only declares skills. This works like a shareable ESLint configuration preset:
{
"name": "@antfu/skills",
"dependencies" {
"agent-browser": "^1.0.0"
},
"skills": [
"npm:agent-browser",
"vercel-labs/agent-skills@react-best-practices",
{ "source": "antfu/skills", "ref": "v2", "skills": ["vue", "vitest"] }
]
}Teams can publish packages like @vercel/skills, indivisuals can publish packages like @antfu/skills. A project adds the pack as a dev dependency, and skills experimental_sync installs every skill it declares. The pack is versioned in the package manager lock file, so the whole team gets the same set of skills.
npm:<package> means the skills that <package> ships. Any other string is a source that skills add accepts, except a local path. The full grammar is the skills-npm SPEC.
Where a package keeps skills
my-lib/
SKILL.md # the whole package is one skill
skills/<name>/SKILL.md
dist/skills/<name>/SKILL.md
Features
- Read direct dependencies: only packages the project depends on
- Link, do not copy: skills update with the package
- Name by the skill: same names as
skills add - Resolve conflicts safely: never shadow or replace what the user installed
- Remove what is gone: uninstall a package, the skill goes too
- Follow
skillsfields: a package declares skills from other packages or Git - Extended lock file: shipped skills tracked in
skills-lock.json - Filters:
--include,--exclude - Scan cache: fast repeat runs
- Workspaces:
--recursive
Read direct dependencies
Sync reads dependencies and devDependencies from the project's package.json. It looks only at those packages. Transitive packages are reached through npm:<package> in a skills field.
Link, do not copy
The canonical directory .agents/skills/<name> becomes a relative symlink into node_modules:
.agents/skills/slidev -> ../../node_modules/@slidev/cli/skills/slidev
Agent directories such as .claude/skills/<name> link to the canonical directory, as they do for skills add. When the package updates, the skill updates with it. The links are committed to Git. Sync does not edit .gitignore.
--copy copies instead of linking. The Eve agent always copies, because it rewrites SKILL.md on install.
Before npm install runs, the links point at nothing. skills list does not show them until the packages are installed.
Name by the skill, not the package
The installed name is the sanitized name from the frontmatter, the same rule that skills add uses. There is no npm- prefix. A SKILL.md without name and description is reported and skipped.
Resolve conflicts safely
When two skills share a name, sync applies these rules in order:
- A skill that the user installed with
skills addalways wins. Sync never shadows it. - Sync never replaces a directory or a link that it does not own. It warns and skips.
- A skill shipped in a package beats a skill declared as a remote source.
- A skill from a direct dependency beats a skill from a transitive dependency.
- If two skills still tie, sync installs neither and warns.
Sync owns a link when its target is under node_modules/. Sync owns a directory when skills-lock.json says its source is node_modules.
Remove what is gone
After each run, sync removes links whose target is under node_modules/ and that this run did not produce. It also removes remote skills that no skills field requests anymore. --no-cleanup turns this off.
Follow skills fields
Sync reads the skills field of the project and of every direct dependency. When an npm: entry points at a package, sync reads that package's skills field too. A visited set stops cycles.
If the project's own field names a package that is not installed, sync exits with an error. If a dependency's field does, sync warns and skips. A malformed entry in the project's field is an error. A malformed field in a dependency is ignored with a warning.
Remote entries install in the same process through the same code path as skills add. --no-remote skips them.
Extended lock file
Sync writes to skills-lock.json. An entry for a shipped skill:
"slidev": {
"source": "@slidev/cli",
"sourceType": "node_modules",
"skillPath": "skills/slidev",
"version": "52.0.0",
"computedHash": "sha256:...",
"via": "@my-org/skill-pack"
}via names the package whose skills field requested the skill. For the project's own field it is .. Remote skills that a field requested also carry via. Sync uses it to know which entries it can remove.
Filters
--include <patterns...> and --exclude <patterns...> match a package name or a skill name. * matches inside one path segment, ** matches across segments, ? matches one character.
Scan cache
Sync caches the scan result in node_modules/.cache/skills/scan.json. The key is a hash of the package manager lock file plus the scan options. --force ignores the cache.
Workspaces
--recursive also reads each workspace package. Sync finds them from pnpm-workspace.yaml and from workspaces in package.json.
Other flags
--dry-run prints the plan and changes nothing.
Not ported
- The
skills-npm.config.tsconfiguration file. Theskillsfield is the declarative surface. - Probing
$PATHfor agent binaries. The CLI already detects agents. - The
setupcommand. The README documents thepreparepattern instead.
Steps
Each step is one PR and ships on its own. Steps 1 to 9 live under skills experimental_sync. Step 10 makes it skills sync.
- Step 1: Scan. Read direct dependencies. Look in root
SKILL.md,skills/,dist/skills/. Name by frontmatter. AddskillPathandversionto the lock entry. (#2349) - Step 2: Link. Add a
linkinstall mode toinstallSkillForAgent. Apply conflict rules 1, 2, 4, and 5. Add--copyand--dry-run. (#2353) - Step 3: Cleanup. Remove stale links and lock entries. Add
--no-cleanup. (#2398) - Step 4:
--includeand--exclude. (#2399) - Step 5:
skillsfield withnpm:entries. Parse per SPEC, resolve from the declaring package, follow chains, recordvia. (#2400) - Step 6: Extract the resolve-discover-install core of
runAddinto a function that returns results and never exits.runAddandexperimental_installcall it. No user-facing change. (#2401, #2402) - Step 7: Remote entries in the
skillsfield. Use the function from step 6. Recordvia. Remove orphans. Add--no-remote. Apply conflict rule 3. (#2403) - Step 8: Scan cache.
- Step 9:
--recursive. - Step 10: Graduate to
skills sync. Keepexperimental_syncas an alias. Document the command, theskillsfield, and thepreparepattern in the README.
Activity
antfubot commentedon Oct 9, 2026
Status update on the steps:
Step 8 (scan cache): dropped. Since step 1, sync reads only direct dependencies, so the scan is small. I measured
experimental_sync --dry-runon a synthetic project with 500 direct dependencies, 10 of which ship skills. It takes about 370 to 470 ms, against a CLI startup of about 175 ms with no dependencies. The scan therefore costs about 200 to 300 ms in that extreme case, and typical projects with tens of direct dependencies spend tens of milliseconds. A cache keyed on the package manager lock file would also go stale when the project's ownskillsfield or a linked workspace package changes, because neither changes the lock file.Filters (step 4): the syntax changed in #2399.
--exclude fooexcludes packagefoo, and--exclude foo#barexcludes only skillbarof packagefoo. A bare pattern matches package names only.--includeuses the same syntax.Remaining: step 9 (
--recursive) and step 10 (graduate toskills sync, docs).docs: expand experimental_sync section and link RFC vercel-labs#2323
mkondratek commentedon Oct 10, 2026
Step 6 (#2401) extracted exactly what an embedding host needs: a resolve → discover → install function that returns results and never exits. I've opened #2455 asking to export it (and the related primitives) as a documented entry point, e.g.
skills/apiviapackage.jsonexports, with types and a progress/log hook in place of the spinner.Would step 10 be a good point to do that, since it already touches the public surface and the README? Cline Desktop would consume it directly instead of spawning
npx skills, and it would remove the reasonskills-npmimports the internals through a submodule. Happy to do the PR once you're past steps 9 and 10, so it doesn't collide with your work onadd.ts/install.ts.gtbuchanan commentedon Oct 30, 2026
Where does agent detection (
getDetectedAgents) fall on this list? One of the biggest ancillary wins ofskills-npmwas that it didn't dump a bunch of unnecessary agent directories into my worktree. I currently pass the result ofgetDetectedAgentstoskillsso I don't have to configure it explicitly in a config.