Skip to content
Back to skills

Neovim

BSecurity

Neovim is a terminal text editor configured in Lua, with a built-in LSP client, Treesitter parsing and, since 0.12, a built-in plugin manager (vim.pack). Use this skill to install Neovim, write or repair an init.lua, add plugins, wire up language servers with vim.lsp.config and vim.lsp.enable, set up format-on-save, or run Neovim headless for scripted edits. Trigger phrases: "set up neovim", "fix my init.lua", "neovim LSP not working", "lspconfig deprecated warning", "add a plugin to nvim", "...

  • 155 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added October 4, 2026
developmenttypescriptpythongobashreactgitapi

Works with

  • terminal
  • cli
  • api

Security analysis

B84/100
  • mediumUses curl or wget to download content
  • criticalModifies startup scripts or system services for persistence

Pro scans all 2 files and shows the line behind each finding

Scanned October 4, 2026

npx -y skills add TerminalSkills/skills --skill neovim --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Neovim?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Neovim
[![Security: B — Skills Directory](https://www.skillsdirectory.com/api/skills/terminalskills-neovim/badge)](https://www.skillsdirectory.com/skills/terminalskills-neovim)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: neovim
description: >-
  Neovim is a terminal text editor configured in Lua, with a built-in LSP client,
  Treesitter parsing and, since 0.12, a built-in plugin manager (vim.pack). Use
  this skill to install Neovim, write or repair an init.lua, add plugins, wire up
  language servers with vim.lsp.config and vim.lsp.enable, set up format-on-save,
  or run Neovim headless for scripted edits. Trigger phrases: "set up neovim",
  "fix my init.lua", "neovim LSP not working", "lspconfig deprecated warning",
  "add a plugin to nvim", "nvim --headless", "format on save in neovim".
license: Apache-2.0
compatibility: "Neovim 0.12+ for vim.pack (0.11+ for vim.lsp.config/enable); git for plugin installs; language servers installed separately"
metadata:
  author: terminal-skills
  version: "1.0.0"
  category: development
  tags: ["neovim", "lua", "lsp", "treesitter", "text-editor"]
  repository: https://github.com/neovim/neovim
---

# Neovim — Lua-configured terminal editor with built-in LSP

## Overview

Neovim (`nvim`) is a Vim-based editor that runs in a terminal and is configured with Lua. It ships an LSP client (diagnostics, go-to-definition, rename, formatting), Treesitter (syntax trees for highlighting, folding and queries) and, from 0.12, `vim.pack`, a Git-based plugin manager with a lockfile. The current stable line is 0.12.x (0.12.5 at the time of writing).

An agent cannot usefully drive the interactive UI, but it can do most of the setup work: install the binary, write `init.lua`, pin plugins, configure language servers, check the config for errors and run Neovim headless for batch edits. This skill covers those jobs.

## Instructions

### Installation

Package managers first; check the version afterwards, because distribution packages often lag behind and `vim.pack` needs 0.12.

```bash
brew install neovim            # macOS or Linux with Homebrew
winget install Neovim.Neovim   # Windows
sudo pacman -S neovim          # Arch
nvim --version | head -1       # expect NVIM v0.12.x
```

If the distro package is too old, use the official release archive (download, then extract as a separate step):

```bash
VER=v0.12.5
curl -LO https://github.com/neovim/neovim/releases/download/$VER/nvim-linux-x86_64.tar.gz
# GitHub records a SHA-256 digest for every release asset; compare before extracting
WANT=$(curl -s https://api.github.com/repos/neovim/neovim/releases/tags/$VER \
  | jq -r '.assets[] | select(.name == "nvim-linux-x86_64.tar.gz") | .digest' | cut -d: -f2)
echo "$WANT  nvim-linux-x86_64.tar.gz" | sha256sum -c -   # must print OK
sudo tar -C /opt -xzf nvim-linux-x86_64.tar.gz
echo 'export PATH="/opt/nvim-linux-x86_64/bin:$PATH"' >> ~/.bashrc
```

Prepending puts the new binary ahead of an older `/usr/bin/nvim`. Archives exist for `linux-arm64`, `macos-arm64` and `macos-x86_64` too.

### Config layout

- `~/.config/nvim/init.lua` — entry point (`:echo stdpath('config')` shows the real path).
- `~/.config/nvim/lua/<name>.lua` — modules loaded with `require('<name>')`.
- `~/.config/nvim/lsp/<server>.lua` — a language-server config returned as a table.
- `~/.config/nvim/after/lsp/<server>.lua` — overrides that win over plugin-provided configs.
- `~/.config/nvim/nvim-pack-lock.json` — the `vim.pack` lockfile; commit it with the config.

To try a config without touching the user's, set `NVIM_APPNAME`: Neovim then reads `~/.config/<name>` and keeps separate data and state directories.

```bash
NVIM_APPNAME=nvim-trial nvim
```

### Plugins with vim.pack (0.12+)

`vim.pack.add()` clones missing plugins into the data directory (`site/pack/core/opt`) and loads them. It needs `git` and is marked experimental in the docs.

```lua
vim.pack.add({
  'https://github.com/neovim/nvim-lspconfig',
  { src = 'https://github.com/nvim-treesitter/nvim-treesitter', version = 'main' },
})
```

- `vim.pack.update()` fetches updates and opens a review buffer: `:write` applies, `:quit` discards. `vim.pack.update(nil, { force = true })` skips the review.
- `vim.pack.update(nil, { target = 'lockfile' })` moves plugins back to the lockfile revisions.
- To remove a plugin, first delete it from the `add()` list and `:restart`, then run `vim.pack.del({ 'plugin-name' })`. `del()` refuses a plugin that is still loaded unless you pass `{ force = true }`.
- Build steps go in a `PackChanged` autocommand defined before `vim.pack.add()`.

Third-party managers such as lazy.nvim still work; do not mix two managers for the same plugin.

### Language servers (vim.lsp.config / vim.lsp.enable)

Neovim provides the client; servers are separate programs on `PATH`. nvim-lspconfig ships ready-made configs in its `lsp/` directory, so after installing it one line per server is enough:

```lua
vim.lsp.enable({ 'pyright', 'ruff', 'ts_ls', 'lua_ls' })
```

Customise with `vim.lsp.config()`; it merges over the plugin's defaults:

```lua
vim.lsp.config('lua_ls', {
  settings = { Lua = { runtime = { version = 'LuaJIT' } } },
})
vim.lsp.config('pyright', { root_markers = { 'pyproject.toml', '.git' } })
vim.lsp.config('*', {   -- fallback for fields a server's own config leaves unset
  capabilities = { textDocument = { semanticTokens = { multilineTokenSupport = true } } },
})
```

`'*'` has the lowest priority: nvim-lspconfig's `lsp/<server>.lua` files override it, so setting `root_markers` there changes nothing. Set root markers per server, as above, or in `after/lsp/<server>.lua`.

A server that nvim-lspconfig does not know needs `cmd`, `filetypes` and `root_markers`. The old `require('lspconfig').<server>.setup{}` framework is deprecated; replace each call with `vim.lsp.config` (settings) plus `vim.lsp.enable` (activation).

Built-in keymaps once a server attaches: `K` hover, `grn` rename, `grr` references, `gri` implementation, `gra` code action, `gO` document symbols, `CTRL-S` signature help in insert mode, and `CTRL-]` jumps to the definition through 'tagfunc'. `:lsp restart` and `:lsp stop` manage clients.

### Format on save and completion

Some servers (ruff, for example) register formatting only after they attach, so check capability when saving rather than inside `LspAttach`:

```lua
vim.api.nvim_create_autocmd('BufWritePre', {
  group = vim.api.nvim_create_augroup('my.format', {}),
  callback = function(ev)
    if #vim.lsp.get_clients({ bufnr = ev.buf, method = 'textDocument/formatting' }) > 0 then
      vim.lsp.buf.format({ bufnr = ev.buf, timeout_ms = 2000 })
    end
  end,
})

vim.api.nvim_create_autocmd('LspAttach', {
  group = vim.api.nvim_create_augroup('my.lsp', {}),
  callback = function(ev)
    local client = assert(vim.lsp.get_client_by_id(ev.data.client_id))
    if client:supports_method('textDocument/completion') then
      vim.lsp.completion.enable(true, client.id, ev.buf, { autotrigger = true })
    end
  end,
})
```

With several formatters attached, pass `name = 'ruff'` or a `filter` function to `vim.lsp.buf.format()`.

### Treesitter

Neovim bundles parsers for C, Lua, Markdown, Vimscript, Vimdoc and Treesitter queries. Other languages need parsers, usually from nvim-treesitter (its `main` branch requires Neovim 0.12, `tree-sitter-cli` and a C compiler; the old `master` branch is frozen).

```lua
require('nvim-treesitter').install({ 'python', 'typescript', 'tsx' })

vim.api.nvim_create_autocmd('FileType', {
  pattern = { 'python', 'typescript', 'typescriptreact', 'lua' },
  callback = function(ev)
    -- pcall: the parser may not be installed yet (install() is asynchronous)
    if pcall(vim.treesitter.start, ev.buf) then
      vim.wo[0][0].foldexpr = 'v:lua.vim.treesitter.foldexpr()'
      vim.wo[0][0].foldmethod = 'expr'
    end
  end,
})
```

Without the `pcall`, every buffer of a language whose parser is missing raises "Parser could not be created". To install parsers before first use, for example on a new machine, wait for the install in a headless run:

```bash
nvim --headless -c "lua require('nvim-treesitter').install({ 'python', 'typescript', 'tsx' }):wait(300000)" -c qa
```

After updating nvim-treesitter, run `:TSUpdate` so parsers match the plugin.

### Headless and scripted use

```bash
nvim --headless +qa                                  # load the config, install vim.pack plugins, exit
nvim --headless "+checkhealth vim.lsp" "+write! lsp-health.txt" +qa
nvim --headless -c 'lua vim.pack.update(nil, { force = true })' -c qa
nvim -l scripts/report.lua src/app.lua               # run a Lua script with Neovim's API, then exit
```

- `nvim -l script.lua args…` skips the user config, exposes arguments as `arg[1]…`, and sends `print()` to stderr; use `io.write()` for stdout.
- `nvim --clean` starts with no user config or plugins — the fastest way to tell a config bug from a Neovim bug.
- `nvim --headless +qa` exits 0 even when `init.lua` fails. To get a failing exit code, test `v:errmsg`: `nvim --headless -c 'if v:errmsg != "" | cquit 1 | endif' -c qa`.

## Examples

### Example 1: Python IDE features over SSH

**User request:** "Set up Neovim on the build box for our `invoicing` Python service: pyright for types, ruff for lint and format on save."

```bash
uv tool install pyright   # PyPI wrapper; provides pyright-langserver
uv tool install ruff
```

`~/.config/nvim/init.lua`:

```lua
vim.g.mapleader = ' '
vim.o.number = true
vim.o.expandtab = true
vim.o.shiftwidth = 4
vim.o.undofile = true

vim.pack.add({ 'https://github.com/neovim/nvim-lspconfig' })
vim.lsp.enable({ 'pyright', 'ruff' })

vim.diagnostic.config({ virtual_text = true })
vim.keymap.set('n', '<leader>e', vim.diagnostic.open_float, { desc = 'Show diagnostic' })

vim.api.nvim_create_autocmd('BufWritePre', {
  group = vim.api.nvim_create_augroup('my.format', {}),
  callback = function(ev)
    if #vim.lsp.get_clients({ bufnr = ev.buf, method = 'textDocument/formatting' }) > 0 then
      vim.lsp.buf.format({ bufnr = ev.buf, timeout_ms = 2000 })
    end
  end,
})
```

```bash
nvim --headless +qa        # clones nvim-lspconfig and writes nvim-pack-lock.json
```

**Result:** opening `billing.py` inside the project (which has a `pyproject.toml`) attaches both servers with the project as root. Ruff flags "`os` imported but unused" and "Undefined name `totl`" inline, and `:w` rewrites `def total(items:list[float])->float:` as `def total(items: list[float]) -> float:` with two blank lines around top-level definitions.

### Example 2: Remove the lspconfig deprecation warning

**User request:** "After updating plugins I get 'The `require('lspconfig')` framework is deprecated, use vim.lsp.config'. Fix my config for TypeScript and Lua."

Old code:

```lua
require('lspconfig').ts_ls.setup({ on_attach = on_attach })
require('lspconfig').lua_ls.setup({ settings = { Lua = { diagnostics = { globals = { 'vim' } } } } })
```

Replacement (keep nvim-lspconfig installed — its `lsp/` configs are still used):

```lua
vim.lsp.config('lua_ls', {
  settings = { Lua = { diagnostics = { globals = { 'vim' } } } },
})
vim.lsp.enable({ 'ts_ls', 'lua_ls' })
-- on_attach logic moves into an LspAttach autocommand (see Instructions)
```

```bash
nvim --headless -c 'if v:errmsg != "" | cquit 1 | endif' -c qa && echo config-ok
```

**Result:** `config-ok` prints, the warning is gone, and `:checkhealth vim.lsp` lists `ts_ls` and `lua_ls` under "Enabled Configurations".

### Example 3: Rename a class across a folder without opening the UI

**User request:** "Rename `BillingClient` to `LedgerClient` in every file under `handlers/`."

```bash
nvim --clean --headless -c 'argdo %s/\<BillingClient\>/LedgerClient/ge | update' -c qa handlers/*.py
grep -rn "LedgerClient" handlers/
```

**Result:** `handlers/refunds.py` and `handlers/invoices.py` are rewritten; `handlers/health.py`, which had no match, is left untouched because `update` writes only modified buffers. `\<…\>` keeps `BillingClientError` from being changed. For symbol-aware renames that follow imports, use `grn` with a language server instead.

## Guidelines

- Check `nvim --version` before writing config: `vim.pack` needs 0.12, `vim.lsp.config`/`vim.lsp.enable` need 0.11. Current nvim-lspconfig requires 0.11.3 or newer.
- Language servers are not bundled. Install them with the ecosystem's package manager and confirm the binary (`pyright-langserver`, `ruff`, `typescript-language-server`) is on `PATH` for the process that starts Neovim.
- Root markers (`pyproject.toml`, `package.json`, `.git`…) decide the workspace root. Without one, most servers (pyright, ruff, lua_ls) still attach in single-file mode with no root, so cross-file features are weaker. Servers marked `workspace_required` (eslint, biome, tailwindcss) do not attach at all outside a project. `:checkhealth vim.lsp` shows the resolved `cmd`, filetypes and root markers.
- Put the user's own overrides in `vim.lsp.config()` or `after/lsp/`, not in the plugin directory; plugin updates overwrite it.
- Commit `nvim-pack-lock.json` so another machine installs the same plugin revisions; do not edit it by hand.
- Before editing someone's config, back it up by copying, or work under `NVIM_APPNAME`. Do not delete the data directory to "fix" plugins; use `vim.pack.del()` or `vim.pack.update(nil, { target = 'lockfile' })`.
- Plugins are code from third parties that runs with the user's permissions. Pin to tags with `version = vim.version.range('1.0')` where a plugin publishes semver tags, and review `vim.pack.update()` changes before `:write`.
- Headless regex edits are text replacements, not refactors: they ignore scope, strings and comments. Use them for mechanical changes and check the diff afterwards.
- Do not use this skill for editor-agnostic tasks (formatting in CI, linting) — call the formatter or linter directly; for VS Code or Zed, use their own settings.

Files in this skill

  • SKILL.md13.4 KB
  • _scores.json1.4 KB

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…