[ ⌘K ]
← BACK TO SEARCH

bigcodegen/mcp-neovim-server

critical

Control Neovim using Model Context Protocol (MCP) and the official neovim/node-client JavaScript library

This MCP server connects to a running Neovim instance via a Unix socket, exposing Neovim's editing, navigation, search, and window management capabili...

purpose: This MCP server connects to a running Neovim instathreat: local only
TypeScript313May 20, 2026May 20, 2026GITHUB
anthropicclaudemcpmodelcontextprotocolneovim
5/20/2026
medium1 finding
src/index.ts
352server.tool(
353  "vim_file_open",
354  "Open files into new buffers",
355  {
356    filename: z.string().describe("Path to the file to open")
357  },
358  async ({ filename }) => {
359    try {
360      const result = await neovimManager.openFile(filename);
361      return {
362        content: [{
363          type: "text",
364          text: result
365        }]
366      };
367    } catch (error) { ... }
368  }
369);
src/index.ts:352

// Local-only MCP, requires compromised LLM to exploit

The vim_file_open tool accepts a filename parameter as a string with no validation or sanitization. It passes the user-supplied path directly to neovimManager.openFile(), which likely opens the file in Neovim. An attacker (compromised LLM) could read arbitrary files on the system, such as /etc/passwd, SSH keys, or other sensitive files, by providing absolute paths or path traversal sequences like '../../etc/passwd'.

ImpactAn attacker could read any file the Neovim process has access to, leading to information disclosure of sensitive data (credentials, configuration, source code).

FixValidate and restrict the filename parameter to a specific working directory. Use path.resolve() and check that the resolved path is within an allowed base directory. Alternatively, reject absolute paths and path traversal sequences.

medium1 finding
src/index.ts
326server.tool(
327  "vim_buffer_save",
328  "Save current buffer or save to specific filename",
329  {
330    filename: z.string().optional().describe("Optional filename to save buffer to (defaults to current buffer's filename)")
331  },
332  async ({ filename }) => {
333    try {
334      const result = await neovimManager.saveBuffer(filename);
335      return {
336        content: [{
337          type: "text",
338          text: result
339        }]
340      };
341    } catch (error) { ... }
342  }
343);
src/index.ts:326

// Local-only MCP, requires compromised LLM to exploit

The vim_buffer_save tool accepts an optional filename parameter with no validation. It passes the user-supplied path directly to neovimManager.saveBuffer(). An attacker could write arbitrary content to any file the Neovim process can write to, potentially overwriting system files, configuration files, or planting malicious scripts.

ImpactAn attacker could write arbitrary data to any file on the system, leading to data corruption, privilege escalation (e.g., overwriting ~/.ssh/authorized_keys), or code execution.

FixValidate and restrict the filename parameter to a specific working directory. Use path.resolve() and ensure the resolved path is within an allowed base directory. Reject absolute paths and path traversal sequences.

medium1 finding
src/index.ts
99server.tool(
100  "vim_command",
101  "Execute Vim commands with optional shell command support",
102  { command: z.string().describe("Vim command to execute (use ! prefix for shell commands if enabled)") },
103  async ({ command }) => {
104    try {
105      if (command.startsWith('!')) {
106        const allowShellCommands = process.env.ALLOW_SHELL_COMMANDS === 'true';
107        if (!allowShellCommands) {
108          return {
109            content: [{
110              type: "text",
111              text: "Shell command execution is disabled..."
112            }]
113          };
114        }
115      }
116      const result = await neovimManager.sendCommand(command);
117      ...
118    } catch (error) { ... }
119  }
120);
src/index.ts:99

// Local-only MCP, requires compromised LLM to exploit

The vim_command tool accepts any Vim command string and executes it via neovimManager.sendCommand(). While shell commands are gated by an environment variable, arbitrary Vim commands can still be used to read/write files (e.g., :r /etc/passwd, :w! /tmp/evil), execute arbitrary Ex commands, and potentially escape the intended editing scope. The tool's purpose is to execute Vim commands, but the scope is too broad as it allows any Vim command, including those that can read/write arbitrary files.

ImpactAn attacker could use Vim commands to read arbitrary files, write to arbitrary files, or execute shell commands (if enabled), leading to information disclosure, data corruption, or code execution.

FixRestrict the allowed Vim commands to a safe subset (e.g., only navigation, editing, and search commands). Alternatively, implement a whitelist of allowed commands or validate that the command does not contain file I/O operations.

medium1 finding
src/index.ts
99server.tool(
100  "vim_command",
101  "Execute Vim commands with optional shell command support",
102  { command: z.string().describe("Vim command to execute (use ! prefix for shell commands if enabled)") },
103  async ({ command }) => {
104    try {
105      // Check if this is a shell command
106      if (command.startsWith('!')) {
107        const allowShellCommands = process.env.ALLOW_SHELL_COMMANDS === 'true';
108        if (!allowShellCommands) {
109          return {
110            content: [{
111              type: "text",
112              text: "Shell command execution is disabled. Set ALLOW_SHELL_COMMANDS=true environment variable to enable shell commands."
113            }]
114          };
115        }
116      }
117
118      const result = await neovimManager.sendCommand(command);
src/index.ts:9

// Local-only MCP, requires compromised LLM to exploit. Only exploitable if ALLOW_SHELL_COMMANDS=true is set.

The vim_command tool allows executing arbitrary Vim commands, and when the ALLOW_SHELL_COMMANDS environment variable is set to 'true', it also allows shell commands via the '!' prefix. This is a documented feature, but it exposes arbitrary shell execution capability that goes beyond the intended purpose of editing text. Even though it is gated by an environment variable, if enabled, a compromised LLM can execute arbitrary shell commands on the host.

ImpactAn attacker (compromised LLM) could execute arbitrary shell commands on the host system, leading to full system compromise, data exfiltration, or persistent access.

FixRemove shell command support entirely, or add additional restrictions such as a whitelist of allowed commands, or require user confirmation for shell commands.

medium1 finding
src/index.ts
73server.tool(
74  "vim_buffer",
75  "Get buffer contents with line numbers",
76  { filename: z.string().optional().describe("Optional file name to view a specific buffer") },
77  async ({ filename }) => {
78    try {
79      const bufferContents = await neovimManager.getBufferContents(filename);
80      return {
81        content: [{
82          type: "text",
83          text: Array.from(bufferContents.entries())
84            .map(([lineNum, lineText]) => `${lineNum}: ${lineText}`)
85            .join('\n')
86        }]
87      };
88    } catch (error) { ... }
89  }
90);
src/index.ts:73

// Local-only MCP, requires compromised LLM to exploit

The vim_buffer tool accepts an optional filename parameter with no validation. It passes the user-supplied path directly to neovimManager.getBufferContents(). An attacker could read arbitrary files by providing absolute paths or path traversal sequences, similar to vim_file_open.

ImpactAn attacker could read any file the Neovim process has access to, leading to information disclosure.

FixValidate and restrict the filename parameter to a specific working directory. Use path.resolve() and check that the resolved path is within an allowed base directory.

medium1 finding
src/index.ts
357server.tool(
358  "vim_file_open",
359  "Open files into new buffers",
360  {
361    filename: z.string().describe("Path to the file to open")
362  },
363  async ({ filename }) => {
364    try {
365      const result = await neovimManager.openFile(filename);
366      return {
367        content: [{
368          type: "text",
369          text: result
370        }]
371      };
src/index.ts:9

// Local-only MCP, requires compromised LLM to exploit

The vim_file_open tool accepts a filename parameter that is passed directly to neovimManager.openFile(). If the underlying implementation does not validate or restrict the path, an attacker can open and read arbitrary files on the system by providing an absolute path or path traversal sequence. The tool's purpose is to open files into buffers for editing, but without path validation it can be abused to read any file.

ImpactAn attacker (compromised LLM) could read any file the Neovim process has access to, including sensitive files like SSH keys, configuration files, or system secrets.

FixValidate that the filename parameter is a relative path within the project directory or restrict it to a whitelist of allowed directories. Use path resolution and ensure it does not escape the intended workspace.

medium1 finding
src/index.ts
332server.tool(
333  "vim_buffer_save",
334  "Save current buffer or save to specific filename",
335  {
336    filename: z.string().optional().describe("Optional filename to save buffer to (defaults to current buffer's filename)")
337  },
338  async ({ filename }) => {
339    try {
340      const result = await neovimManager.saveBuffer(filename);
341      return {
342        content: [{
343          type: "text",
344          text: result
345        }]
346      };
src/index.ts:9

// Local-only MCP, requires compromised LLM to exploit

The vim_buffer_save tool accepts an optional filename parameter that is passed directly to neovimManager.saveBuffer(). If the underlying implementation does not validate or restrict the path, an attacker can write arbitrary content to any file the Neovim process has write access to, by providing an absolute path or path traversal sequence. The tool's purpose is to save the current buffer, not to write arbitrary files.

ImpactAn attacker (compromised LLM) could overwrite critical system files, inject malicious code into startup scripts, or modify configuration files, potentially leading to privilege escalation or persistent compromise.

FixValidate that the filename parameter is a relative path within the project directory or restrict it to existing buffer names only. Use path resolution and ensure it does not escape the intended workspace.

medium1 finding
src/index.ts
76server.tool(
77  "vim_buffer",
78  "Get buffer contents with line numbers",
79  { filename: z.string().optional().describe("Optional file name to view a specific buffer") },
80  async ({ filename }) => {
81    try {
82      const bufferContents = await neovimManager.getBufferContents(filename);
83      return {
84        content: [{
85          type: "text",
86          text: Array.from(bufferContents.entries())
87            .map(([lineNum, lineText]) => `${lineNum}: ${lineText}`)
88            .join('\n')
89        }]
90      };
src/index.ts:9

// Local-only MCP, requires compromised LLM to exploit

The vim_buffer tool accepts an optional filename parameter that is passed directly to neovimManager.getBufferContents(). If the underlying implementation does not validate or restrict the path, an attacker can read arbitrary files on the system by providing an absolute path or path traversal sequence (e.g., /etc/passwd or ../../etc/shadow). The tool's purpose is to view buffer contents, not arbitrary files.

ImpactAn attacker (compromised LLM) could read any file the Neovim process has access to, including sensitive files like SSH keys, configuration files, or system secrets.

FixValidate that the filename parameter is a relative path within the project directory or restrict it to existing buffer names only. Use path resolution and ensure it does not escape the intended workspace.

medium1 finding
src/index.ts
437server.tool(
438  "vim_grep",
439  "Project-wide search using vimgrep with quickfix list",
440  {
441    pattern: z.string().describe("Search pattern to grep for"),
442    filePattern: z.string().optional().describe("File pattern to search in (default: **/* for all files)")
443  },
444  async ({ pattern, filePattern = "**/*" }) => {
445    try {
446      const result = await neovimManager.grepInProject(pattern, filePattern);
447      return {
448        content: [{
449          type: "text",
450          text: result
451        }]
452      };
453    } catch (error) { ... }
454  }
455);
src/index.ts:437

// Local-only MCP, requires compromised LLM to exploit

The vim_grep tool accepts a filePattern parameter with no validation. It passes the user-supplied pattern directly to neovimManager.grepInProject(). An attacker could use path traversal in the filePattern to search files outside the intended project directory, potentially reading sensitive files.

ImpactAn attacker could search for patterns in arbitrary files on the system, leading to information disclosure.

FixValidate and restrict the filePattern to a specific working directory. Use path.resolve() and ensure the resolved path is within an allowed base directory. Reject patterns containing '..' or absolute paths.

medium1 finding
src/index.ts
509server.tool(
510  "vim_tab",
511  "Manage Neovim tabs: create, close, and navigate between tabs",
512  {
513    action: z.enum(["new", "close", "next", "prev", "first", "last", "list"]).describe("Tab action to perform"),
514    filename: z.string().optional().describe("Filename for new tab (optional)")
515  },
516  async ({ action, filename }) => {
517    try {
518      const result = await neovimManager.manageTab(action, filename);
519      return {
520        content: [{
521          type: "text",
522          text: result
523        }]
524      };
525    } catch (error) { ... }
526  }
527);
src/index.ts:509

// Local-only MCP, requires compromised LLM to exploit

The vim_tab tool accepts an optional filename parameter with no validation. It passes the user-supplied path directly to neovimManager.manageTab(). An attacker could open arbitrary files in a new tab, reading sensitive files.

ImpactAn attacker could open and read any file the Neovim process has access to, leading to information disclosure.

FixValidate and restrict the filename parameter to a specific working directory. Use path.resolve() and check that the resolved path is within an allowed base directory.

low1 finding
src/index.ts
437server.tool(
438  "vim_grep",
439  "Project-wide search using vimgrep with quickfix list",
440  {
441    pattern: z.string().describe("Search pattern to grep for"),
442    filePattern: z.string().optional().describe("File pattern to search in (default: **/* for all files)")
443  },
444  async ({ pattern, filePattern = "**/*" }) => {
445    try {
446      const result = await neovimManager.grepInProject(pattern, filePattern);
447      return {
448        content: [{
449          type: "text",
450          text: result
451        }]
452      };
src/index.ts:9

// Local-only MCP, requires compromised LLM to exploit

The vim_grep tool accepts a filePattern parameter that defaults to '**/*' and is passed directly to neovimManager.grepInProject(). If the underlying implementation does not restrict the search scope, an attacker can search for patterns across the entire filesystem, potentially reading sensitive files. The tool's purpose is project-wide search, but without path restrictions it can be abused to search arbitrary files.

ImpactAn attacker (compromised LLM) could search for sensitive patterns (e.g., passwords, keys) across the entire filesystem, leading to information disclosure.

FixRestrict the search scope to the current project directory or a configurable workspace. Validate that filePattern does not contain path traversal sequences.

shell.execenv.exposurefilesystem.writeauth.none
100
LLM-based
low findings+5
medium findings+150
scoringcompleted