Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Interact with the user's running Neovim instance via RPC. Use this skill when you need to execute Lua or Vimscript inside Neovim, query buffer state, send commands, or interact with the Neovim runtime in any way. Triggers when the user asks about their current Neovim session, wants to run something inside Neovim, or when you need to inspect Neovim state (buffers, windows, options, LSP, etc.). Also use when running inside a Neovim terminal and needing to communicate with the parent editor.
.claude/skills/fredrikaverpil-neovim/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-14 | ✗→✓ | ▲ Improved | 99% | 0% |
| case-07 | ✗→✓ | ▲ Improved | 81% | 0% |
| case-09 | ✗→✓ | ▲ Improved | 93% | 0% |
| case-16 | ✗→✓ | ▲ Improved | 41% | 0% |
| case-17 | ✗→✓ | ▲ Improved | 101% | 0% |
When Claude Code runs inside a Neovim terminal, the $NVIM environment variable points to the parent Neovim's Unix socket. This gives full access to Neovim's msgpack-RPC API without any plugins or HTTP servers.
Before sending any commands, verify the socket is available:
bashecho "$NVIM"
If $NVIM is empty, you are not running inside a Neovim terminal and cannot communicate with a Neovim instance.
Important: When NVIM_APPNAME is set, all nvim --server commands emit a Warning: Using NVIM_APPNAME=... message on stdout (not stderr). This corrupts parsed output (especially JSON). To suppress it, capture the output first, then filter:
bashresult=$(nvim --server "$NVIM" --remote-expr 'EXPR') && echo "$result" | grep -v '^Warning: Using NVIM_APPNAME='
Note: Piping nvim directly (e.g. nvim --server "$NVIM" ... | grep ...) can fail because $NVIM may not expand correctly in pipe contexts. Always use command substitution ($(...)) as shown above.
All examples below use the command substitution pattern from Prerequisites to filter the NVIM_APPNAME warning. The shorthand nvimx EXPR means:
bashresult=$(nvim --server "$NVIM" --remote-expr 'EXPR') && echo "$result" | grep -v '^Warning: Using NVIM_APPNAME='
Use --remote-expr to evaluate a Vimscript expression and get the result back:
bashresult=$(nvim --server "$NVIM" --remote-expr 'v:version') && echo "$result" | grep -v '^Warning: Using NVIM_APPNAME='
For Lua expressions, wrap them in luaeval():
bashresult=$(nvim --server "$NVIM" --remote-expr 'luaeval("vim.api.nvim_buf_get_name(0)")') && echo "$result" | grep -v '^Warning: Using NVIM_APPNAME='
For multi-statement Lua that returns a value, use an IIFE:
bashresult=$(nvim --server "$NVIM" --remote-expr 'luaeval("(function() local x = vim.api.nvim_get_current_win(); return vim.api.nvim_win_get_number(x) end)()")') && echo "$result" | grep -v '^Warning: Using NVIM_APPNAME='
luaeval() returns Lua tables as Vimscript values. For complex data, encode as JSON:
bashresult=$(nvim --server "$NVIM" --remote-expr 'luaeval("vim.json.encode(vim.api.nvim_list_bufs())")') && echo "$result" | grep -v '^Warning: Using NVIM_APPNAME='
Use --remote-send to send keystrokes (as if the user typed them):
bashnvim --server "$NVIM" --remote-send ':echo "hello"<CR>'
Note: --remote-send does not return output and does not need the warning filter. Use --remote-expr when you need a return value.
Use --remote to open files in the running Neovim instance:
bashnvim --server "$NVIM" --remote file.txt
Use --remote-tab to open files in new tabs:
bashnvim --server "$NVIM" --remote-tab file1.txt file2.txt
To run Lua that performs side effects (no return value needed):
bashresult=$(nvim --server "$NVIM" --remote-expr 'execute("lua vim.notify(\"Hello from Claude\")")') && echo "$result" | grep -v '^Warning: Using NVIM_APPNAME='
The execute() Vimscript function runs an Ex command and returns its output as a string (empty if the command produces no output).
bash# Current buffer path result=$(nvim --server "$NVIM" --remote-expr 'luaeval("vim.api.nvim_buf_get_name(0)")') && echo "$result" | grep -v '^Warning: Using NVIM_APPNAME=' # List all buffer paths (JSON) result=$(nvim --server "$NVIM" --remote-expr 'luaeval("vim.json.encode(vim.tbl_map(function(b) return vim.api.nvim_buf_get_name(b) end, vim.api.nvim_list_bufs()))")') && echo "$result" | grep -v '^Warning: Using NVIM_APPNAME=' # Current working directory result=$(nvim --server "$NVIM" --remote-expr 'luaeval("vim.fn.getcwd()")') && echo "$result" | grep -v '^Warning: Using NVIM_APPNAME=' # Current cursor position [row, col] (1-indexed row) result=$(nvim --server "$NVIM" --remote-expr 'luaeval("vim.json.encode(vim.api.nvim_win_get_cursor(0))")') && echo "$result" | grep -v '^Warning: Using NVIM_APPNAME=' # Get a Neovim option value result=$(nvim --server "$NVIM" --remote-expr 'luaeval("vim.o.filetype")') && echo "$result" | grep -v '^Warning: Using NVIM_APPNAME=' # Check if an LSP client is attached result=$(nvim --server "$NVIM" --remote-expr 'luaeval("vim.json.encode(vim.tbl_map(function(c) return c.name end, vim.lsp.get_clients({bufnr = 0})))")') && echo "$result" | grep -v '^Warning: Using NVIM_APPNAME='
Do not use execute("help ...") — that opens help inside the editor as a side effect instead of returning content.
First, get the key paths via RPC (do this once per session):
bash# Neovim data directory (plugin install root is <data>/lazy/ for lazy.nvim) result=$(nvim --server "$NVIM" --remote-expr 'luaeval("vim.fn.stdpath(\"data\")")') && echo "$result" | grep -v '^Warning: Using NVIM_APPNAME=' # Built-in Neovim docs result=$(nvim --server "$NVIM" --remote-expr 'luaeval("vim.fn.expand(\"$VIMRUNTIME\")")') && echo "$result" | grep -v '^Warning: Using NVIM_APPNAME='
Then use standard tools (fd, rg, Glob, Grep) to search and Read to view the files. Search <data>/lazy/*/doc/ for plugin docs and <runtime>/doc/ for built-in docs.
Search help tags (equivalent to :h query<Tab> completion):
bashresult=$(nvim --server "$NVIM" --remote-expr 'luaeval("vim.json.encode(vim.fn.getcompletion(\"MiniDiff\", \"help\"))")') && echo "$result" | grep -v '^Warning: Using NVIM_APPNAME='
Search runtime files (searches all runtime paths including user config, plugins, and pack/*/start/*):
bash# Find Lua source files matching a keyword (e.g. "codediff", "neotest") result=$(nvim --server "$NVIM" --remote-expr 'luaeval("vim.json.encode(vim.api.nvim_get_runtime_file(\"lua/**/neotest*\", true))")') && echo "$result" | grep -v '^Warning: Using NVIM_APPNAME=' # Find any runtime file by pattern (plugin/, autoload/, syntax/, etc.) result=$(nvim --server "$NVIM" --remote-expr 'luaeval("vim.json.encode(vim.api.nvim_get_runtime_file(\"**/neotest*\", true))")') && echo "$result" | grep -v '^Warning: Using NVIM_APPNAME='
Note: nvim_get_runtime_file only searches active runtime paths. Lazy-loaded plugins that haven't been loaded yet won't appear. See the lazy.nvim section below for how to find those.
Then use Read, Glob, or Grep to explore the returned paths.
The plugin manager lazy.nvim uses its own directory layout, separate from Neovim's built-in pack/ structure.
Plugins are installed under stdpath("data")/lazy/ (e.g. ~/.local/share/nvim-fredrik/lazy/<plugin-name>/). This path is not part of the standard Neovim packpath.
The lazy.nvim API knows about all plugins regardless of whether they are loaded:
bash# Get a specific plugin's directory result=$(nvim --server "$NVIM" --remote-expr 'luaeval("require(\"lazy.core.config\").plugins[\"neotest\"].dir")') && echo "$result" | grep -v '^Warning: Using NVIM_APPNAME=' # List all plugins with their paths (JSON) result=$(nvim --server "$NVIM" --remote-expr 'luaeval("vim.json.encode(vim.tbl_map(function(p) return {name = p.name, dir = p.dir, dev = p.dev or false} end, vim.tbl_values(require(\"lazy.core.config\").plugins)))")') && echo "$result" | grep -v '^Warning: Using NVIM_APPNAME='
You can also search the install directory directly with fd/Glob using the stdpath("data")/lazy/ path.
dev = true)Plugins with dev = true in their spec are loaded from a local development path instead of the install directory.
bash# Get the dev path from lazy.nvim config result=$(nvim --server "$NVIM" --remote-expr 'luaeval("require(\"lazy.core.config\").options.dev.path")') && echo "$result" | grep -v '^Warning: Using NVIM_APPNAME=' # Check if a specific plugin is using dev mode result=$(nvim --server "$NVIM" --remote-expr 'luaeval("require(\"lazy.core.config\").plugins[\"codediff.nvim\"].dev")') && echo "$result" | grep -v '^Warning: Using NVIM_APPNAME='
A dev plugin's source lives at <dev.path>/<plugin-name> (e.g. if dev.path is ~/code/public, then codediff.nvim with dev = true loads from ~/code/public/codediff.nvim). The plugin's .dir field in the lazy API already reflects this.
Plugin specifications (the Lua files that configure which plugins to load) live in the Neovim config directory, not in the install directory. Search there when you need to find how a plugin is configured:
bash# Find plugin spec files result=$(nvim --server "$NVIM" --remote-expr 'luaeval("vim.fn.stdpath(\"config\")")') && echo "$result" | grep -v '^Warning: Using NVIM_APPNAME=' # Then use Glob/Grep to search the returned config path
When files are edited externally (e.g. by Claude Code tools), Neovim's LSP diagnostics can become stale — showing warnings for old line numbers or already-fixed issues. To refresh:
bashresult=$(nvim --server "$NVIM" --remote-expr 'execute("lua vim.api.nvim_buf_call(BUFNR, function() vim.cmd(\"edit! | write\") end)")') && echo "$result" | grep -v '^Warning: Using NVIM_APPNAME='
bashresult=$(nvim --server "$NVIM" --remote-expr 'execute("LspRestart")') && echo "$result" | grep -v '^Warning: Using NVIM_APPNAME='
After restarting, wait ~10 seconds for the LSP server to re-index before querying diagnostics again.
to confirm the actual state (e.g. golangci-lint run ./... for Go).
:q, :qa, :bdelete, or other destructive commands withoutexplicit user confirmation.
have unsaved work or an undo history they care about.
--remote-expr (read-only queries) over --remote-send (simulatestyping) whenever possible.
grep -v to suppress the NVIM_APPNAMEwarning (see Prerequisites).
For common Neovim workflows (LSP interaction, debugging, plugin management), see the references/ directory.
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-14 | fail→pass | 10,717 | 4,979 | -54% | 1 | 1 | 0% | 2,056 | 4,089 | +99% | 0 | 0 | — |
case-01 | fail→fail | 4,490 | 9,458 | +111% | 1 | 1 | 0% | 803 | 3,794 | +372% | 0 | 0 | — |
case-02 | fail→fail | 13,241 | 4,664 | -65% | 1 | 1 | 0% | 2,612 | 3,460 | +32% | 0 | 0 | — |
case-03 | fail→fail | 7,708 | 5,799 | -25% | 1 | 1 | 0% | 1,113 | 3,538 | +218% | 0 | 0 | — |
case-04 | pass→pass | 11,492 | 7,558 | -34% | 1 | 1 | 0% | 2,237 | 4,693 | +110% | 0 | 0 | — |
case-05 | pass→pass | 7,660 | 5,941 | -22% | 1 | 1 | 0% | 1,449 | 4,202 | +190% | 0 | 0 | — |
case-06 | pass→pass | 11,394 | 10,705 | -6% | 1 | 1 | 0% | 2,199 | 5,198 | +136% | 0 | 0 | — |
case-07 | fail→pass | 13,710 | 8,597 | -37% | 1 | 1 | 0% | 2,690 | 4,877 | +81% | 0 | 0 | — |
case-08 | pass→pass | 12,828 | 4,139 | -68% | 1 | 1 | 0% | 2,518 | 3,940 | +56% | 0 | 0 | — |
case-09 | fail→pass | 11,804 | 4,249 | -64% | 1 | 1 | 0% | 2,063 | 3,985 | +93% | 0 | 0 | — |
case-10 | pass→pass | 14,249 | 5,254 | -63% | 1 | 1 | 0% | 2,516 | 4,093 | +63% | 0 | 0 | — |
case-11 | pass→pass | 12,331 | 6,241 | -49% | 1 | 1 | 0% | 2,280 | 4,343 | +90% | 0 | 0 | — |
case-12 | pass→pass | 12,340 | 4,402 | -64% | 1 | 1 | 0% | 2,038 | 3,920 | +92% | 0 | 0 | — |
case-13 | pass→pass | 9,539 | 3,083 | -68% | 1 | 1 | 0% | 1,663 | 3,649 | +119% | 0 | 0 | — |
case-15 | fail→fail | 12,245 | 6,406 | -48% | 1 | 1 | 0% | 2,148 | 4,302 | +100% | 0 | 0 | — |
case-16 | fail→pass | 13,759 | 3,149 | -77% | 1 | 1 | 0% | 2,595 | 3,660 | +41% | 0 | 0 | — |
case-17 | fail→pass | 12,845 | 5,967 | -54% | 1 | 1 | 0% | 2,132 | 4,288 | +101% | 0 | 0 | — |
case-18 | pass→pass | 10,840 | 3,509 | -68% | 1 | 1 | 0% | 1,872 | 3,718 | +99% | 0 | 0 | — |
case-19 | pass→pass | 17,378 | 6,301 | -64% | 1 | 1 | 0% | 3,114 | 4,368 | +40% | 0 | 0 | — |
case-20 | fail→pass | 13,627 | 4,948 | -64% | 1 | 1 | 0% | 2,456 | 4,045 | +65% | 0 | 0 | — |
case-21 | pass→pass | 12,398 | 6,509 | -47% | 1 | 1 | 0% | 2,005 | 4,182 | +109% | 0 | 0 | — |
case-22 | fail→fail | 14,695 | 10,548 | -28% | 1 | 1 | 0% | 2,322 | 4,788 | +106% | 0 | 0 | — |
case-23 | fail→fail | 9,416 | 3,781 | -60% | 1 | 1 | 0% | 1,753 | 3,802 | +117% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 23 cases were attempted, and 20 counted toward the lift figure. The other 3 produced results that are not comparable between the two arms, so they are excluded from the headline rather than averaged into it. The headline lift of +26 percentage points is the difference between those two pass rates over the 20 comparable cases. 1 case got worse with the skill loaded, and it is included in that figure.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.