vlt skills
Usage:
$ vlt skills [list|link] [<query>] [--view=human|json|mermaid]Manage the agent skills shipped by your installed dependencies.
An agent skill is a folder with a SKILL.md file, as defined by the
Agent Skills specification.
Packages can ship them alongside their code, and vlt skills links
them into your project's agent skill dirs so coding agents can find
them.
How vlt finds skills
vlt only looks in installed packages, it never downloads skills:
- Install the package as usual, e.g.
vlt install agent-browser. - A DSS query picks the packages to look in:
allow-skillson install, or the query given tovlt skills(*forlistby default). vltscans each picked package for skills (below), then keeps the ones:skill()picks.- Each skill is linked into the project's agent skill dirs, named after the skill. Agents read skills from these dirs.
vlt skills list shows what vlt finds, and vlt query ':skills'
lists the packages that ship skills.
Which packages
- Packages installed in the
node_modules/.vltstore, direct or transitive. - Not the project or its workspaces, and not
file:dependencies (folders and tarballs). - One version per package: the one the project depends on directly, else a workspace's, else the highest version, even if it ships no skills. Different origins (e.g. registry and git) are different packages.
Where in a package
vlt looks for skills in each package, in this order:
SKILL.mdat the package root – a single skill, named after thenamefield of its frontmatter (or the package name, without its scope)skills/<name>/SKILL.md– one folder per skill, named<name>dist/skills/<name>/SKILL.md– same, for built packages.agents/skills/<name>/SKILL.md– same
A package can ship skills in several of these places. When the same skill name (in any case) is in more than one, the first one in this order is used.
What counts as a skill
- A folder with a
SKILL.mdfile, directly in one of the dirs above:skills/group/<name>/SKILL.mdis not found. - Folder names use letters, digits,
.,_and-, and start with a letter or digit. Other folders, and one namedSKILL.md, are skipped. - Symlinks are not followed: a symlinked skill folder or
SKILL.md, or a symlinked dir on the way (skills/,dist/,dist/skills/,.agents/,.agents/skills/), is skipped, so a skill always lives inside its package. - A root
SKILL.mdis named after its frontmatternamewhen that is a valid skill name (lowercase letters, digits and single-, up to 64 characters), else after the package. vltreadsnameanddescriptionfrom theSKILL.mdfrontmatter. A missing or invalid frontmatter doesn't hide the skill.:skill()matches the skill name or the frontmattername.
Shipping skills in a package
my-tool/
├── package.json
├── dist/
└── skills/
└── my-skill/
└── SKILL.md- Publish the skills: if
package.jsonhasfiles, add the skill dirs (or a rootSKILL.md) to it. Check withvlt pack --dry-run. - A package that is a single skill can put
SKILL.mdat its root. - Skills built into
dist/skills/are found too.
Same name in different packages
When packages ship skills with the same name, one gets the link in
every dir: the package an existing link points to, while that skill is
installed, else a direct dependency, then the package id. The others
are reported. Installs keep a dir's link to another installed skill;
vlt skills link re-points it. Narrowing the query and running
vlt skills link <query> frees a name. A fresh clone (no links yet)
may pick another package than a machine with older links. :skill()
applies first: a skill the query doesn't pick never takes a name.
A skill isn't found
vlt skills list "#<package>"lists whatvltfinds in it.- Check the package is installed from a registry, git or a remote
tarball, not a workspace or
file:dependency. - Check the layout and folder name rules above.
vlt skills linkand installs warn about a:skill()pattern that matches nothing, listing the skills its packages ship.- With a security selector such as
:not(:malware), packages with no security data are left out and reported. See Safety.
Where skills are linked
Unless skills-dir is set, skills are linked into:
.agents/skills, always. Codex, Cursor, GitHub Copilot, Gemini CLI and many other agents read it.<folder>/skillsfor each agent folder at the project root:.claude/gives.claude/skills,.kiro/gives.kiro/skills, and so on (.tabnine/gives.tabnine/agent/skills,.posit/assistant/gives.posit/assistant/skills). Missing dirs are created. Only the project root is checked, not workspaces..claude/skillswhen run inside Claude Code (CLAUDECODEis set), even without a.claude/folder. Once created,.claude/keeps it a target outside Claude Code too.- Any existing
.<name>/skillsdir, such as.cursor/skillsor.github/skills. These are never created.
.github/ alone is not an agent folder. Home dirs (~/.claude) are
not read, so in a plain terminal a project with no .claude/ gets
.agents/skills only: run mkdir .claude, or set skills-dir.
Dirs are created only when the query picks a skill.
skills-dir
Set skills-dir to a list of dirs, relative to the project root, to
replace detection:
# link into .claude/skills only
$ vlt config set skills-dir=.claude/skills
# or for one install
$ vlt install --allow-skills="*" --skills-dir=.claude/skills- A dir outside the project or in
node_modulesis an error; one that links outside is skipped. []invlt.json(or--skills-dir=) links nothing.nullinvlt.jsongoes back to detection.- This is also how to drop
.agents/skillsor an unwanted agent dir.
Links in a dir that is no longer a target stay until the next
vlt skills link, which removes them. A custom dir dropped from
skills-dir (not named .<name>/skills) is no longer scanned: remove
its links by hand.
Symlinked dirs
A symlinked dir or agent folder is followed when it points inside the
project, and skipped and reported when it points outside or into
node_modules. So is a dir that can't be read or created. A dir
reached twice (e.g. .claude/skills -> ../.agents/skills) gets each
link once.
Pick skills
End a selector with :skill(<name>, …) to link only some of its
packages' skills:
# one skill of one package
$ vlt skills link "#skills:skill(find-skills)"
$ vlt skills link '#skills:skill("find-skills")'
# some skills of every direct dependency
$ vlt skills link ":root > *:skill(performance, find-skills)"
# globs
$ vlt skills link ":root > *:skill(vercel*, *-review)"
# any package, same as *:skill(find-skills)
$ vlt skills link ":skill(find-skills)"- A pattern matches the skill name (its folder name, or the root
SKILL.mdname) or thenamein itsSKILL.mdfrontmatter, in any case.*matches any run of characters. Quote names with spaces::skill("my skill"). - A selector without
:skill()picks every skill of its packages, and comma lists are a union. So:root > *, #skills:skill(find-skills)links every skill of every direct dependency,skillsincluded. To narrow one package, leave it out of the other selectors::root > *:not(#skills), #skills:skill(find-skills). :skill()must end a top-level selector:#a:skill(x) > *is an error.:skills(no()) matches packages that ship skills.:skills(x)is an error (did you mean:skill(x)?) on its own, and matches nothing inside a comma list.- A pattern that matches no skill of its packages prints a warning listing the skills they ship. The install still succeeds.
Preview a query with vlt skills list '<query>' before link saves
it. See :skill().
Layout
Each skill is linked into every dir, named after the skill:
.agents/skills/
├── lint-rules -> ../../node_modules/.vlt/~npm~foo@1.0.0/node_modules/foo/skills/lint-rules
└── bar -> ../../node_modules/.vlt/~npm~@scope+bar@2.0.0/node_modules/@scope/bar
.claude/skills/
├── lint-rules -> ../../node_modules/.vlt/~npm~foo@1.0.0/node_modules/foo/skills/lint-rules
└── bar -> ../../node_modules/.vlt/~npm~@scope+bar@2.0.0/node_modules/@scope/barOn Windows, links are directory junctions. Junctions hold absolute
paths, so after moving or copying the project, run
vlt skills link <query> (or vlt install with allow-skills set)
to re-point them.
Subcommands
list
The default. Lists the skills of installed packages as a tree, like
vlt ls: <skill> (<package>), marked (linked) when linked in
every agent skill dir, (linked in <dirs>) when only in some. With
colors on, linked skills are highlighted like the packages vlt query
selects. Warnings follow the tree. Selects all packages (*) by
default and applies :skill(). Creates and saves nothing.
$ vlt skills
my-project
├── find-skills (skills@1.7.2) (linked)
└── get-tsconfig (get-tsconfig@5.0.0-beta.6)
$ vlt skills list ":root > *"
$ vlt skills list "#skills:skill(find-*)"--view=json: each skill'snameis the skill, then itsdescription,dependency-namethe dependency it is installed as,package-idits package id,pathand the dirs it islinkedin;dirslists the agent skill dirs.--view=mermaid: a Mermaid flowchart of the dependency graph, asvlt query --view=mermaiddraws it, from the project down to each package with skills, its skills as highlighted leaves. Warnings follow as%%comments. List only.
$ vlt skills list --view=mermaid
flowchart TD
a("root:my-project")
a -->|"skills#64;github:vercel-labs/skills"| b("git(github:vercel-labs/skills):skills#64;1.7.2")
b -->|"skills#64;1.7.2"| f("find-skills (linked)"):::b
b -->|"obuild#64;^0.4.22 (dev)"| c("npm:obuild#64;0.4.45")
c -->|"rolldown-plugin-dts#64;^0.28.8"| d("npm:rolldown-plugin-dts#64;0.28.8")
d -->|"get-tsconfig#64;5.0.0-beta.6"| e("npm:get-tsconfig#64;5.0.0-beta.6")
e -->|"get-tsconfig#64;5.0.0-beta.6"| g("get-tsconfig"):::b
classDef b fill:turquoise,color:#242424link
Sets, saves and applies allow-skills. Links what the query selects
and removes vlt links to other skills in every scanned dir: skills
the query doesn't pick, skills that are gone, and links in dirs that
are no longer targets. It is the only command that removes links to
installed skills. With no query, it uses the stored allow-skills,
else :not(:malware), like vlt build.
Then it saves the query when it changed: to the project vlt.json, or
to the user one with --config=user. A query in the other file is
never deleted: a project value shadows the user one.
| Query given | Stored | -y | TTY | Result |
|---|---|---|---|---|
| yes | no | any | any | link it, save it |
| yes | yes | no | yes | Replace it? [y/N]: y opens the editor with the query, N changes nothing |
| yes | yes | yes | any | link it, save it |
| no | yes | no | yes | Edit it? [y/N]: y opens the editor with the stored query, N links it |
| no | yes | yes | any | link the stored query |
| no | no | no | yes | the editor, prefilled with :not(:malware) |
| no | no | yes | any | link :not(:malware), save it |
| yes | yes | no | no | error: pass -y to replace it |
| no | yes | no | no | link the stored query |
| no | no | no | no | link :not(:malware), save it |
A query equal to the stored one links without asking or writing.
$ vlt skills link ":root > *:skill(find-skills)"
allow-skills is already set for this project (vlt.json): ":root > *". Replace it? [y/N] y
⚠️ The new allow-skills replaces the current one. Make sure it still
selects every skill you want: skills it no longer selects are unlinked.
Current allow-skills: :root > *
New allow-skills: :root > *:skill(find-skills)
1 agent skill already linked.
Saved allow-skills=":root > *:skill(find-skills)" to vlt.jsonIn the editor, the new query is prefilled: edit it and press Enter. An
empty answer cancels with no changes, and an invalid query prints the
error and asks again. -y skips the prompts. Current and New are
colored when colors are on.
# a single package, no prompts
$ vlt skills link "#my-package" -y
# save to the user vlt.json
$ vlt skills link "#my-package" --config=user
# remove every skill link
$ vlt skills link ":not(*)" -ySelect packages with a DSS query after the
subcommand, or with --target=<query>, which wins over the positional
argument.
Add skill packages one at a time:
$ vlt install github:vercel-labs/skills
$ vlt skills link "#skills:skill(find-skills)" # saved to vlt.json
$ vlt install agent-skills@github:vercel-labs/agent-skills
$ vlt skills link
# Edit it? y
# New allow-skills: #skills:skill(find-skills), #agent-skills:skill(react*)
# on another machine, both are linked
$ vlt ciLinking on install
With allow-skills unset (the default) or :not(*), vlt install,
vlt ci, vlt update and vlt uninstall do no skills work at all:
nothing is scanned, created or removed, and leftover links stay until
vlt skills link.
With allow-skills set, on the command line or in vlt.json, they
link what it selects after a successful install:
# link, and save the query to vlt.json
$ vlt install --allow-skills="#agent-browser:skill(agent-browser)"
Saved allow-skills="#agent-browser:skill(agent-browser)" to vlt.json- Installs only add links. They re-point a link to the new version of
its package, in every dir, and remove links whose skill is gone:
dangling, package removed, or skill no longer shipped. They never
remove links to installed skills: after narrowing
allow-skills, runvlt skills link. vlt installandvlt updatesave--allow-skills=<query>to the projectvlt.json. When another query is stored (:not(*)too), it is kept and a hint says so: change it withvlt skills link.vlt cinever writes config: in a fresh clone, it links whatvlt.jsonselects.--lockfile-onlylinks and saves nothing.vlt uninstallwith a stored query removes the removed package's links.- Dir changes take effect for new links; old ones go on the next
vlt skills link. An invalid query orskills-dirfails before anything is installed.
Safety
vltonly ever replaces or removes links that point into anode_modules/.vltstore. Anything else at a link path (your own skills, other tools' copies or links) is skipped and reported for that dir, never changed.- Dirs are never removed.
- Security queries fail closed, like
allow-scripts: with a security selector such as:not(:malware), skills of npm registry packages with no security data (the request timed out, or Socket has not scanned them yet) are not linked and are reported. Installs keep their existing links,vlt skills linkremoves them: try again later. See Security. - Linked skills are views of installed package files: don't edit them, a reinstall replaces them. Files hardlinked from the global store get private copies when linked, so edits stay in this project.
Ignoring linked skills in git
Links dangle in a fresh clone until install, so vlt keeps their
names in a marked block of each dir's .gitignore:
# >>> vlt agent skills (links into node_modules)
/bar
/lint-rules
# <<< vlt agent skillsLines are added for new links and removed with them, or when your own
entry takes the name. The rest of the file is kept, and the file is
never deleted: committed, it keeps the dir in a fresh clone. A dir
with no vlt links is never touched. A .gitignore that is a
symlink, not a file or can't be written is left alone and reported.
Limits
- Skills nested deeper than
<dir>/<name>/SKILL.mdare not found. - Skills are linked at the project root only, not per workspace.