vp-astgrep
Live ast-grep structural-lint diagnostics in Claude Code, via the
ast-grep language server.
Your .ast-grep/rules/*.yml already run on ast-grep scan. This makes them run as the code is
written — a violation surfaces in-context right after the edit, instead of minutes later on a
check run or in CI.
Why
A convention written in a comment enforces nothing.
The rule that motivated this plugin was born when a file's own header said "import it, don't re-roll
it" — and six call sites re-rolled it anyway, for months, past code review. An ast-grep rule caught
all six, plus a seventh that a hand-written grep had missed (the grep keyed on an identifier; the rule
keyed on the shape).
That is the case for structural rules. This plugin is the case for making them immediate.
Install
/plugin marketplace add voxpelli/vp-claude
/plugin install vp-astgrep@vp-plugins
Requires the ast-grep binary on $PATH:
brew install ast-grep # or: npm i -g @ast-grep/cli, cargo install ast-grep
And, since 0.4.0, Node ≥ 20 on $PATH — the language server now runs behind a small stdio shim
(see below), and the shim is a Node script. This is a real new requirement and worth stating bluntly:
No node on Claude Code's PATH ⇒ the server does not start ⇒ you get no diagnostics at all.
That is a worse failure than the stale rules 0.4.0 fixes, so it is the one thing to check first.
The trap is nvm/fnm: they put node on the PATH of your shell, not of your desktop session. So
node -v working in a terminal does not guarantee Claude Code can see it — if you launched Claude
Code from Spotlight or the Dock rather than from that shell, it may not. If diagnostics vanish after
upgrading, run claude --debug and look for a spawn failure on node; launching Claude Code from a
shell where node -v works is the quickest fix.
If you would rather not take the Node dependency, pin 0.3.1 — it invokes ast-grep directly, and
its only cost is that rule edits need a restart. You can also keep 0.4.0 and set
LSP_SHIM_DISABLE=1, which bypasses the shim and reverts to exactly 0.3.1's behaviour at runtime.
Requirements — read this before filing a bug
Your project must have an sgconfig.yml in its root. The ast-grep language server hard-exits
without one:
Error: No ast-grep project configuration is found.
So in a repo with no sgconfig.yml, this plugin is a no-op — the server fails to start, Claude
Code skips it, and you get no diagnostics. That is expected, not a defect. A minimal config:
# sgconfig.yml
ruleDirs:
- .ast-grep/rules
Run claude --debug to see language-server startup errors.
What it registers
"lspServers": {
"ast-grep": {
"command": "node",
"args": [
"${CLAUDE_PLUGIN_ROOT}/bin/lsp-shim.mjs", // <- the shim, since 0.4.0
"ast-grep", "lsp", "-c", "${CLAUDE_PROJECT_DIR}/sgconfig.yml"
],
"extensionToLanguage": { ".js": "javascript", /* …14 more, see below */ }
}
}
The explicit -c is load-bearing. The server otherwise resolves sgconfig.yml from its working
directory and exits if it isn't there; passing the path makes it independent of whatever cwd the
client happens to use. This exact race is a known cross-editor failure mode.
Rule hot-reload, and the shim that makes it work
Edit a rule, then edit some code, and the new rule applies. No restart. That is new in 0.4.0, and
0.3.1 shipped the opposite as a documented limitation — this section is a retraction as much as a
feature note.
The bug it works around
ast-grep's server has no file watcher of its own. On initialized it sends the client a
client/registerCapability request, asking it to watch **/*.{yml,yaml} and report back via
workspace/didChangeWatchedFiles. Claude Code replies -32601 "Unhandled method" and watches nothing.
So the server is never told the rules changed, and serves its startup rule set forever.
The symptom is what makes this expensive: document sync keeps working perfectly. Edit a source file
and the server re-analyses it instantly, correct line numbers and all — while rule edits do nothing at
all. Two channels; only one is wired. So the plugin looks perfectly healthy, and you conclude your rule
is wrong rather than simply never loaded.
Except it was never actually silent — and this is the part worth knowing. ast-grep notices the
refusal and reports it, immediately, naming the exact cause:
[window/logMessage ERROR] Failed to register file watchers:
Error { code: MethodNotFound, message: "Unhandled method client/registerCapability" }