A language server for agent skills — /skill-name
and $skill-name
become real symbols in VS Code, Zed, Neovim, and Helix.
▸ completion w/ descriptions on /
and $
▸ "did you mean" on typos
▸ go to definition, find references
▸ F2 rename w/ editor undo — folder, frontmatter name:
, and every reference update as one edit
▸ clickable links + semantic highlighting on resolved references
Your skill library now has tooling.
Skills, CLAUDE.md
/AGENTS.md
, and agent files reference each other by name — a dependency graph with all the refactoring hazards of code and none of the tooling. Rename a skill by hand and stale references look like ordinary prose: nothing errors, the agent silently stops the skill.
Quiet by design — a false reference in prose costs more than a missed one:
- Fenced code blocks are never parsed; inline code spans are (that's how people write skill names in prose)
/usr/bin
,$PATH
,docs/
— never references, never popups- Near miss of a real skill (edit distance ≤ 2) →
did you meanwarning; other unresolved/name
→ info hint; unresolved$name
→ silent - Built-in commands (
/help
,/compact
, …) and your own.claude/commands
/.codex/prompts
are commands, not skills — never flagged
It's a language server, not a linter — pair with skill-lint
or agnix
for structural/security linting.
npm install -g skill-language-server
Then wire up your editor below. To see it work: cursor on any /skill-name
, hit F2
, type a new name — folder, frontmatter, and every reference update as one undo step.
[language-server.skill-language-server]
command = "skill-language-server"
args = ["--stdio"]
[[language]]
name = "markdown"
language-servers = ["skill-language-server"] # add e.g. "marksman" here if you use it
Create ~/.config/nvim/lsp/skill-language-server.lua
:
return {
cmd = { "skill-language-server", "--stdio" },
filetypes = { "markdown" },
root_markers = { ".claude", ".git" },
}
Add vim.lsp.enable("skill-language-server")
to init.lua. (Or from a clone: { dir = "/path/to/skill-language-server/ext/nvim" }
in lazy.nvim.)
Command palette → zed: install dev extension
→ select ext/zed/
from a clone. After server updates: editor: restart language server
.
git clone https://github.com/CyrusNuevoDia/skill-language-server
cd skill-language-server
mise trust && mise install && bun install
just build-vscode
code --install-extension dist/skill-language-server.vsix
(Marketplace listing pending — the .vsix bundles the server.)
mise trust && mise install # bun, just (rust only for the Zed wasm)
bun install
just bin # → ~/.local/bin/skill-language-server
The server's world is the folder your editor opened — it never reads outside it. A rename touches exactly the tree you have open, never another checkout or your home directory. Open ~/.claude
itself as a workspace to refactor your global library.
Skills= any**/skills/<name>/SKILL.md
. Folder name is canonical; a disagreeing frontmattername:
is an error, not an aliasReferences are scanned in.md
files under.claude/
,.agents/
,.codex/
, orskills/
, plus everyCLAUDE.md
/AGENTS.md
. Markdown elsewhere is never touchedLive index where the editor supports LSP file watching; open buffers beat disk. Without watching, the index catches up on file open or restart(gitignore syntax, workspace root) excludes paths from everything.skillignore
- Multi-root workspaces: only the first folder is indexed
Cross-workspace renames are a deliberate two-step: rename where the skill lives, then open the other workspace — stale references surface as hints/warnings there. Blind spots: $
stragglers stay silent, and an old name that doubles as a built-in command reads as the built-in.
Checked against client source as of 2026-07: VS Code, Zed, Helix, and Neovim all apply the folder RenameFile
.
- Neovim never sends
willRenameFiles
— explorer-drag renames don't rewrite references there; rename from a token or the frontmatter instead - File watching: VS Code and Zed yes; Neovim yes except off by default on Linux; Helix only sees its own edits — reopen the file or restart the server after external changes
just check # tsc (server + VS Code extension) + ultracite lint
bun test # protocol-level tests against a fixture workspace
just build # everything into dist/ — binary, .vsix, Zed wasm (wasm needs rust)
just fmt # ultracite fix --unsafe
The test suite is the contract: tests/harness.ts
boots the real server over in-memory streams; tests/corpus.ts
is the ground-truth reference set. Done = just check
bun test
green.
Three layers in src/
: parse.ts
(token grammar), workspace.ts
(index), server.ts
(LSP wiring). Editor shims in ext/{vscode,zed,nvim}
.
Releases via changesets: bun changeset
with your change, merge to main, CI publishes.
MIT