Library / SDK
neovim/node-client avatar
neovim/node-client

node-client: running JavaScript inside Neovim as a remote plugin

Nvim Node.js client and plugin host

536 stars58 forksTypeScriptMIT

At a glance

What is it?
The neovim node-client package gives you two things in one: an API client that speaks Neovim's message-pack RPC from Node, and a plugin host that discovers and runs Node-based remote plugins inside Neovim itself. The design detail that shapes everything is that stdout is the RPC channel, so the module replaces console at load time, and every workaround that follows, from ALLOW_CONSOLE to the file logger, exists because of that.
Who is it for?
Adopt node-client if you want to write Neovim plugins in JavaScript or drive Neovim from a Node script, because it is the officially supported client for the editor's message-pack RPC and it exposes the editor as typed wrapper objects rather than raw messages. Do not write a remote plugin that logs to stdout or does work at import time, since the module takes stdout away from you and the host may load your plugin only to inspect it.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 2 days ago.
What is it written in?
Mainly TypeScript, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 22, 2026, and from our analysis. They are not legal advice.

Editorial analysis

What the package is, and why it is two things at once

The neovim node-client package is described as an Nvim Node.js client and plugin host, and the README splits the project into two roles that live in one package.

The first is the API client, which talks to nvim. The second is the remote plugin host, which discovers and runs Nvim node.js remote plugins. Both are needed because a remote plugin, in Neovim's sense, is a plugin implemented as an Nvim API client, so the host has to speak the protocol in order to run something that itself speaks the protocol.

Neovim supports remote plugins, which are plugins implemented as Nvim API clients, and you can define one as a file or folder in an rplugin/node/ directory on Neovim's runtimepath. If the plugin is a folder, the main script from package.json will be loaded. The plugin must export a function which takes a NvimPlugin object as its only parameter, and you may then register autocmds, commands and functions by calling methods on that object.

The primary language is TypeScript, the licence is MIT, the default branch is master, and the homepage is neovim.io/node-client. The last push was on 2026-10-19 with the project not archived, and the repository publishes no GitHub releases, so the npm package is the distribution channel. The package version in the manifest reads 5.5.1-dev.0, so the published line is on 5.x and the repository is ahead of it.

The CI badge is worth reading for what it covers: CI runs on node 16 and later across Linux, macOS and Windows, with a separate coverage badge.

Node.js 16 and later is tested. Older versions of Node.js may work but are not tested. That distinction between what is tested and what may work is stated plainly, which is a good habit in a package that is a dependency of somebody's editor configuration.

Installing it, and the global versus local split that catches people

The install section splits on how you intend to use the package, and the reason for the split is a Node behaviour rather than a design choice.

For remote plugins, the Nvim Node.js provider expects the neovim package to be globally installed:

bash
npm install -g neovim

Or for non-plugin purposes, neovim works like any other NPM package.

The quickstart then says to install the neovim package locally in any directory, that is, without the -g flag:

bash
   npm install neovim
   # Or if build from source:
   npm run build

The reason is spelled out in the parenthetical: Node throws ERR_MODULE_NOT_FOUND if a script imports a globally installed package. So for a script you run yourself, a local install is what works, and for a remote plugin that Neovim launches and resolves itself, a global install is what the provider expects.

The second line of the local install is the build-from-source path, npm run build, for when you have cloned the repository.

There is also a subtlety in the remote plugin case that is easy to miss. When Neovim launches a remote plugin, it may only be collecting information about the plugin without wishing to actually use it. The README is direct about the consequence: avoid heavy initialisation or async functions at this stage, and instead wait for one of your autocmds, commands or functions to be called before starting any processing.

That is an unusual constraint to write down, and it comes from the fact that plugin discovery is not the same as plugin activation. A plugin that starts work at import time will do work every time Neovim starts, whether or not the plugin is used that session.

The console patching, and why stdout is sacred

The Logging section is the part of the README that explains the package's most surprising behaviour.

At load-time, the neovim module replaces, in the words of the README, monkey patches, console with its logger interface, so console.log will call logger.info instead of writing to stdout, which would break the stdio RPC channel.

That is the whole key to the design. Neovim's stdio transport is message-pack over stdin and stdout, so anything you print to stdout is, from Neovim's point of view, a malformed RPC message. A single console.log in the wrong place corrupts the channel. So the library takes stdout away from you at import time, which is a fairly aggressive move, and then gives you a logger that writes somewhere safe.

The escapes from that rule are documented, and they are all environment variables. To skip the patching of console.log, pass a custom logger to attach(). The best practice in any case is to use the logger available from the NeovimClient returned by attach(), instead of console logging functions. Set the $NVIM_NODE_LOG_FILE env var to also write logs to a file. And set the $ALLOW_CONSOLE env var to also write logs to stdout, with the warning that this will break any stdio RPC channel because logs written to stdout are invalid RPC messages.

So the logging model has three destinations, chosen by environment, and one of them is explicitly unsafe in one of the two transports.

The practical consequence for a plugin author is that you are not free to use console.log in a remote plugin, and that constraint is enforced for you rather than left to documentation. It also explains the one thing the README asks twice, which is that ALLOW_CONSOLE is only for demo purposes, and it cannot be used for remote plugins or whenever stdio is an RPC channel.

Set $NVIM_NODE_LOG_FILE as well and you get both, since the variable says also, so the file gets the logs while stdout stays clean.

The demo, and what the API actually looks like

The quickstart gives a complete, working example, and it is worth reading because the API shape is unusual in a useful way.

The script finds nvim on the system and opens a channel to it:

js
   import * as child_process from 'node:child_process';
   import * as assert from 'node:assert';
   import { attach, findNvim } from 'neovim';

   // Find `nvim` on the system and open a channel to it.
   (async function () {
     const found = findNvim({ orderBy: 'desc', minVersion: '0.9.0' });
     console.log(found);
     const [arg0, ...args] = found.matches[0].cmd;
     const nvim_proc = child_process.spawn(arg0, [...args, '--clean', '--embed'], {});
     const nvim = attach({ proc: nvim_proc });

     nvim.command('vsp | vsp | vsp');

     const windows = await nvim.windows;

Several things about that are worth pulling out.

findNvim is called with orderBy: 'desc' and minVersion: '0.9.0', and it returns a structure with a matches array whose first entry has a cmd. So the binary is not found by a fixed path, it is searched for and version-checked, with a preference for newer versions. Then the process is spawned with --clean and --embed, and attach takes the process.

The API is then a set of properties that are promises. nvim.command runs a command. nvim.windows is awaited and gives an array of Window objects, and the assertion checks that windows[0] is an instance of nvim.Window. Then nvim.window is assigned to select a window, and awaiting it gives the selected one, with the id compared against the array. And nvim.buffer is awaited to get a Buffer, whose lines property is awaited to get the text, and buf.replace writes new lines back.

So the object model is exposed directly, and the type checks are real: the demo asserts instanceof against nvim.Window and nvim.Buffer, which means the client returns genuine wrapper objects rather than plain data.

The demo also handles shutdown carefully. If the process has a disconnect method it is called, nvim.quit() is issued, and then the script polls the exit code every hundred milliseconds until it is set, logging while it waits.

The two functions the package provides are attach(), which takes a process, a socket, or a pair of write and read streams and returns a NeovimClient connected to an nvim process, and findNvim(), which tries to find a usable nvim binary on the current system.

The first signature is the important one, because it says the client is transport-agnostic. Stdio is the default for plugins, but a socket or a stream pair works too, which is what lets the same code drive a remote Neovim over TCP.

The plugin API, and the dev and alwaysInit options

The remote plugin API section documents the NvimPlugin object, and the beginning of it is the piece that tells you what a plugin is given.

NvimPlugin.nvim is the nvim api object you can use to send commands from your plugin to nvim, and the options interface is:

ts
  NvimPlugin.setOptions(options: NvimPluginOptions);

  interface NvimPluginOptions {
    dev?: boolean;
    alwaysInit?: boolean;
  }

```

Set your plugin to dev mode, which will cause the module to be reloaded on each invocation. The alwaysInit option will always attempt to re-instantiate the plugin, for example your plugin class will always get called on each invocation of your plugin's command.

Those two options exist because the plugin host has to decide when to construct your plugin, and there are three reasonable behaviours. Load it once and keep it, load it fresh every time, or never construct it until something actually calls it. dev mode gives you the second for development, where you are editing the plugin file and want the change to take effect, and alwaysInit gives you the second for production, where you want the plugin to be re-instantiated on each use so that any state it holds is fresh.

The autocmd registration comes next, and the options interface there has three fields with specific meanings. Pattern is the only required option, and the comment points at the help for autocmd-pattern. Eval is a Vimscript expression evaluated by the Nvim peer. And sync forces blocking, non-async, behaviour.

The eval option is the one that makes the client more than a wrapper. A Vimscript expression evaluated on the Neovim side lets a plugin ask the editor a question in the editor's own terms, which is the thing you often actually need. The sync option exists because by default autocmds, commands and functions are all treated as asynchronous and should return Promises or be async functions, so if you need a synchronous answer you have to say so.

The registerAutocmd signature has two overloads, one taking a function and one taking an array of any and Function. The second is for the case where you want to call a method on an object, and passing the object and its method lets the host keep the reference rather than losing it to a bound function.

There are also examples in the repository, and the example file names describe the different styles a plugin can take: class.md, decorators.md, functional.md and prototype.md, plus an rplugin/ directory and its own README.

How to evaluate it, and what to check first

There are four things to consider.

First, whether you want the plugin host or just the client. These are separable in practice even though they ship together, and the decision changes what you read. If you are writing a Node script that drives Neovim, the client and its promise-based object model are what matter, and the plugin lifecycle rules are irrelevant. If you want a plugin that Neovim loads and that registers commands and autocmds, then the host, the runtimepath placement, the discovery behaviour and the console patching are your problem.

Second, the console patching, and this is the thing that will surprise you. The module replaces console at import time, so a plugin that uses console.log will find it going somewhere unexpected, and enabling ALLOW_CONSOLE will corrupt the RPC channel. The rule to take away is simple: in a remote plugin, use the logger on the NeovimClient and set NVIM_NODE_LOG_FILE if you want a file copy. The demo sets ALLOW_CONSOLE only because it is a standalone script where stdout is not an RPC channel, and the README says so explicitly.

Third, the discovery-time warning, which is a real trap. Heavy work at module scope means work on every Neovim start, whether or not your plugin is used. Register your commands and autocmds first and start the work in the handler, which is what the README tells you to do.

Fourth, the packaging and version state. The package version is 5.5.1-dev.0, so the npm release line and the repository are not in step, and the repository itself is a Yarn workspace with five packages, the client, the decorators package, the integration tests, and two example plugin packages used both as documentation and as test fixtures. The tooling is modern, with TypeScript 5.9, eslint 10, prettier 3.8, husky and lint-staged, and the lint script ignores the example packages, which is a sensible carve-out since examples are meant to be read rather than to satisfy the project's own rules.

What you get is the officially supported way to write Neovim plugins in JavaScript, with a well-specified transport contract and an object model that exposes the editor as typed wrappers rather than raw RPC. The cost is the constraint it imposes: you do not get stdout, you do not get to do work at import time, and you are on Node 16 as the tested floor.

Editorial conclusion

Adopt node-client if you want to write Neovim plugins in JavaScript or drive Neovim from a Node script, because it is the officially supported client for the editor's message-pack RPC and it exposes the editor as typed wrapper objects rather than raw messages. Do not write a remote plugin that logs to stdout or does work at import time, since the module takes stdout away from you and the host may load your plugin only to inspect it. Before you commit, decide whether you need the global install for the provider or a local one for a script, read the NvimPlugin options and the autocmd sync and eval fields, and put your work in the command or autocmd handler rather than at module scope, so the plugin costs nothing on a Neovim start where nobody uses it.

Frequently asked questions

What is the neovim node-client package?

It is both an API client, which talks to nvim over its message-pack RPC, and a remote plugin host, which discovers and runs Nvim node.js remote plugins. It is MIT licensed and written in TypeScript, and the last push to the repository was on 2026-10-19.

How do I install the neovim package?

For remote plugins, the Nvim Node.js provider expects the neovim package to be globally installed with npm install -g neovim. For a script you run yourself, install it locally with npm install neovim, because Node throws ERR_MODULE_NOT_FOUND if a script imports a globally installed package. You can also build from source with npm run build.

Why does the neovim package replace console?

Because stdout carries the stdio RPC channel, so anything written there is an invalid RPC message and would break the channel. At load time the module replaces console with its logger interface so console.log calls logger.info instead, and you can pass a custom logger to attach() to skip that.

How do I get the neovim logs to a file or to stdout?

Set the $NVIM_NODE_LOG_FILE environment variable to also write logs to a file, and set $ALLOW_CONSOLE to also write logs to stdout. The README warns that ALLOW_CONSOLE will break any stdio RPC channel because logs written to stdout are invalid RPC messages, so it is for demo purposes only and cannot be used for remote plugins.

How do I define a Neovim remote plugin in Node?

Define it as a file or folder in an rplugin/node/ directory on Nvim's runtimepath, and if it is a folder the main script from package.json is loaded. The plugin must export a function taking a NvimPlugin object as its only parameter, and you can then register autocmds, commands and functions on it, avoiding heavy initialisation or async functions at that stage because Nvim may only be collecting information about the plugin.

Official sources

  1. Issues
  2. License: MIT
  3. neovim/node-client on GitHub
  4. Project website
  5. README
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/neovim-node-client.svg)](https://hysenlabs.com/projects/neovim-node-client)

Was this useful?