The first Neovim IDE integration for Claude Code β bringing Anthropic's AI coding assistant to your favorite editor with a pure Lua implementation.
π― TL;DR: When Anthropic released Claude Code with VS Code and JetBrains support, I reverse-engineered their extension and built this Neovim plugin. This plugin implements the same WebSocket-based MCP protocol, giving Neovim users the same AI-powered coding experience.
claudecode-nvim.mp4
When Anthropic released Claude Code, they only supported VS Code and JetBrains. As a Neovim user, I wanted the same experience β so I reverse-engineered their extension and built this.
- π Pure Lua, Zero Dependencies β Built entirely with
vim.loop
and Neovim built-ins - π 100% Protocol Compatible β Same WebSocket MCP implementation as official extensions
- π Fully Documented Protocol β Learn how to build your own integrations (see PROTOCOL.md)
- β‘ First to Market β Beat Anthropic to releasing Neovim support
- π οΈ Built with AI β Used Claude to reverse-engineer Claude's own protocol
{
"coder/claudecode.nvim",
dependencies = { "folke/snacks.nvim" },
config = true,
keys = {
{ "<leader>a", nil, desc = "AI/Claude Code" },
{ "<leader>ac", "<cmd>ClaudeCode<cr>", desc = "Toggle Claude" },
{ "<leader>af", "<cmd>ClaudeCodeFocus<cr>", desc = "Focus Claude" },
{ "<leader>ar", "<cmd>ClaudeCode --resume<cr>", desc = "Resume Claude" },
{ "<leader>aC", "<cmd>ClaudeCode --continue<cr>", desc = "Continue Claude" },
{ "<leader>am", "<cmd>ClaudeCodeSelectModel<cr>", desc = "Select Claude model" },
{ "<leader>ab", "<cmd>ClaudeCodeAdd %<cr>", desc = "Add current buffer" },
{ "<leader>as", "<cmd>ClaudeCodeSend<cr>", mode = "v", desc = "Send to Claude" },
{
"<leader>as",
"<cmd>ClaudeCodeTreeAdd<cr>",
desc = "Add file",
ft = { "NvimTree", "neo-tree", "oil" },
},
-- Diff management
{ "<leader>aa", "<cmd>ClaudeCodeDiffAccept<cr>", desc = "Accept diff" },
{ "<leader>ad", "<cmd>ClaudeCodeDiffDeny<cr>", desc = "Deny diff" },
},
}
That's it! The plugin will auto-configure everything else.
- Neovim >= 0.8.0
- Claude Code CLI installed
- folke/snacks.nvim for enhanced terminal support
" Launch Claude Code in a split
:ClaudeCode
" Claude now sees your current file and selections in real-time!
" Send visual selection as context
:'<,'>ClaudeCodeSend
" Claude can open files, show diffs, and more
- Launch Claude: Run
:ClaudeCode
to open Claude in a split terminal - Send context:
- Select text in visual mode and use
<leader>as
to send it to Claude - In
nvim-tree
/neo-tree
/oil.nvim
, press<leader>as
on a file to add it to Claude's context
- Select text in visual mode and use
- Let Claude work: Claude can now:
- See your current file and selections in real-time
- Open files in your editor
- Show diffs with proposed changes
- Access diagnostics and workspace info
:ClaudeCode
- Toggle the Claude Code terminal window:ClaudeCodeFocus
- Smart focus/toggle Claude terminal:ClaudeCodeSelectModel
- Select Claude model and open terminal with optional arguments:ClaudeCodeSend
- Send current visual selection to Claude:ClaudeCodeAdd <file-path> [start-line] [end-line]
- Add specific file to Claude context with optional line range:ClaudeCodeDiffAccept
- Accept diff changes:ClaudeCodeDiffDeny
- Reject diff changes
When Claude proposes changes, the plugin opens a native Neovim diff view:
- Accept:
:w
(save) or<leader>aa
- Reject:
:q
or<leader>ad
You can edit Claude's suggestions before accepting them.
This plugin creates a WebSocket server that Claude Code CLI connects to, implementing the same protocol as the official VS Code extension. When you launch Claude, it automatically detects Neovim and gains full access to your editor.
The protocol uses a WebSocket-based variant of MCP (Model Context Protocol) that:
- Creates a WebSocket server on a random port
- Writes a lock file to
~/.claude/ide/[port].lock
(or$CLAUDE_CONFIG_DIR/ide/[port].lock
ifCLAUDE_CONFIG_DIR
is set) with connection info - Sets environment variables that tell Claude where to connect
- Implements MCP tools that Claude can call
π Read the full reverse-engineering story β π§ Complete protocol documentation β
Built with pure Lua and zero external dependencies:
- WebSocket Server - RFC 6455 compliant implementation using
vim.loop
- MCP Protocol - Full JSON-RPC 2.0 message handling
- Lock File System - Enables Claude CLI discovery
- Selection Tracking - Real-time context updates
- Native Diff Support - Seamless file comparison
For deep technical details, see ARCHITECTURE.md.
{
"coder/claudecode.nvim",
dependencies = { "folke/snacks.nvim" },
opts = {
-- Server Configuration
port_range = { min = 10000, max = 65535 },
auto_start = true,
log_level = "info", -- "trace", "debug", "info", "warn", "error"
terminal_cmd = nil, -- Custom terminal command (default: "claude")
-- Selection Tracking
track_selection = true,
visual_demotion_delay_ms = 50,
-- Terminal Configuration
terminal = {
split_side = "right", -- "left" or "right"
split_width_percentage = 0.30,
provider = "auto", -- "auto", "snacks", "native", or custom provider table
auto_close = true,
snacks_win_opts = {}, -- Opts to pass to `Snacks.terminal.open()`
},
-- Diff Integration
diff_opts = {
auto_close_on_accept = true,
vertical_split = true,
open_in_current_tab = true,
},
},
keys = {
-- Your keymaps here
},
}
You can create custom terminal providers by passing a table with the required functions instead of a string provider name:
require("claudecode").setup({
terminal = {
provider = {
-- Required functions
setup = function(config)
-- Initialize your terminal provider
end,
open = function(cmd_string, env_table, effective_config, focus)
-- Open terminal with command and environment
-- focus parameter controls whether to focus terminal (defaults to true)
end,
close = function()
-- Close the terminal
end,
simple_toggle = function(cmd_string, env_table, effective_config)
-- Simple show/hide toggle
end,
focus_toggle = function(cmd_string, env_table, effective_config)
-- Smart toggle: focus terminal if not focused, hide if focused
end,
get_active_bufnr = function()
-- Return terminal buffer number or nil
return 123 -- example
end,
is_available = function()
-- Return true if provider can be used
return true
end,
-- Optional functions (auto-generated if not provided)
toggle = function(cmd_string, env_table, effective_config)
-- Defaults to calling simple_toggle for backward compatibility
end,
_get_terminal_for_test = function()
-- For testing only, defaults to return nil
return nil
end,
},
},
})
Here's a complete example using a hypothetical my_terminal
plugin:
local my_terminal_provider = {
setup = function(config)
-- Store config for later use
self.config = config
end,
open = function(cmd_string, env_table, effective_config, focus)
if focus == nil then focus = true end
local my_terminal = require("my_terminal")
my_terminal.open({
cmd = cmd_string,
env = env_table,
width = effective_config.split_width_percentage,
side = effective_config.split_side,
focus = focus,
})
end,
close = function()
require("my_terminal").close()
end,
simple_toggle = function(cmd_string, env_table, effective_config)
require("my_terminal").toggle()
end,
focus_toggle = function(cmd_string, env_table, effective_config)
local my_terminal = require("my_terminal")
if my_terminal.is_focused() then
my_terminal.hide()
else
my_terminal.focus()
end
end,
get_active_bufnr = function()
return require("my_terminal").get_bufnr()
end,
is_available = function()
local ok, _ = pcall(require, "my_terminal")
return ok
end,
}
require("claudecode").setup({
terminal = {
provider = my_terminal_provider,
},
})
The custom provider will automatically fall back to the native provider if validation fails or is_available()
returns false.
- Claude not connecting? Check
:ClaudeCodeStatus
and verify lock file exists in~/.claude/ide/
(or$CLAUDE_CONFIG_DIR/ide/
ifCLAUDE_CONFIG_DIR
is set) - Need debug logs? Set
log_level = "debug"
in opts - Terminal issues? Try
provider = "native"
if using snacks.nvim
See DEVELOPMENT.md for build instructions and development guidelines. Tests can be run with make test
.
- Claude Code CLI by Anthropic
- Inspired by analyzing the official VS Code extension
- Built with assistance from AI (how meta!)