claudecode.nvim

repository·main·Indexed 25 days ago

https://github.com/coder/claudecode.nvim

A pure Lua Neovim integration for Anthropic's Claude Code CLI. It implements the WebSocket-based MCP protocol to bring an AI-powered coding experience into Neovim, featuring terminal integration via providers like snacks.nvim, native diff views for proposing changes, and programmatic text sending. Requires Neovim >= 0.8.0 and the Claude Code CLI.

Tokens
10.9K
Snippets
27
Records
44
Agent score
84%

What's inside claudecode.nvim

  1. Architecture of Claude Code IDE Extensions

    main

    Claude Code IDE extensions function by creating a WebSocket server within the IDE that Claude connects to. The integration relies on a WebSocket variant of the Model Context Protocol (MCP).

    Discovery Workflow:

    1. WebSocket Server: The IDE starts a server on a random port (10000-65535).
    2. Lock File: The IDE writes a discovery file to ~/.claude/ide/[port].lock containing the PID, workspace folders, IDE name, transport type (ws), and a 32-character hex authToken.
    3. Environment Variables: The IDE sets CLAUDE_CODE_SSE_PORT (the port) and ENABLE_IDE_INTEGRATION="true" when launching Claude.
    4. Connection: Claude reads the lock files and connects to the specified port.
  2. Follow implementation guidelines

    main

    When contributing code to claudecode.nvim, adhere to these standards:

    Error Handling

    • All public functions must include error handling.
    • Use the success, result_or_error return pattern.
    • Log meaningful error messages.

    Performance

    • Minimize impact on editor performance.
    • Debounce event handlers.
    • Use asynchronous operations where possible.

    Compatibility

    • Support Neovim >= 0.8.0.
    • Maintain zero external dependencies (pure Lua implementation).
    • Follow Neovim plugin best practices.
  3. Configure environment variables for IDE integration

    main

    To enable Claude Code to connect to your custom IDE integration, you must set the following environment variables before running the claude command:

    • CLAUDE_CODE_SSE_PORT: The port number where your WebSocket server is listening.
    • ENABLE_IDE_INTEGRATION: Set to true to activate the integration.

    Example usage:

    export CLAUDE_CODE_SSE_PORT=12345
    export ENABLE_IDE_INTEGRATION=true
    claude
  4. Build a custom Claude Code IDE integration

    main

    To build a custom integration for Claude Code, you must implement a WebSocket server that follows a specific handshake and discovery protocol. The integration involves four main steps: creating a WebSocket server, writing a lock file for discovery, setting environment variables, and handling JSON-RPC messages.

    -- 1. Create a WebSocket Server (Bind to localhost only!)
    local server = create_websocket_server("127.0.0.1", random_port)
    
    -- 2. Write the Lock File (~/.claude/ide/[port].lock)
    -- Generate a 128-bit token (32-char lowercase hex) using a CSPRNG
    local bytes = vim.loop.random(16)
    local auth_token = bytes:gsub(".", function(c)
      return string.format("%02x", string.byte(c))
    end)
    
    local lock_data = {
      pid = vim.fn.getpid(),
      workspaceFolders = { vim.fn.getcwd() },
      ideName = "YourEditor",
      transport = "ws",
      authToken = auth_token
    }
    write_json(lock_path, lock_data)
    
    -- 3. Set Environment Variables
    -- export CLAUDE_CODE_SSE_PORT=[port]
    -- export ENABLE_IDE_INTEGRATION=true
    
    -- 4. Handle Messages (Validate auth on handshake)
    function validate_auth(headers)
      local auth_header = headers["x-claude-code-ide-authorization"]
      return auth_header == auth_token
    end
    
    -- Example: Send selection updates
    send_message({
      jsonrpc = "2.0",
      method = "selection_changed",
      params = { ... }
    })
    
    -- Example: Implement tools
    register_tool("openFile", function(params)
      return { content = {{ type = "text", text = "Done" }} }
    end)
  5. Manage terminal width during diffs using Diff Lifecycle Events

    main

    If you set diff_opts.auto_resize_terminal = false, you can manually control the terminal width by hooking into the following User autocmds:

    • ClaudeCodeDiffOpened: Fired when a proposed-edit diff opens.
      • event.data fields: tab_name, file_path, new_file_path, is_new_file, diff_window, target_window, terminal_window, tab_number.
    • ClaudeCodeDiffClosed: Fired when the diff is accepted, rejected, or closed.
      • event.data fields: tab_name, file_path, reason (e.g., "diff accepted").

    Example: Manually setting terminal width to 20% of columns on diff open:

    vim.api.nvim_create_autocmd("User", {
      pattern = "ClaudeCodeDiffOpened",
      callback = function(ev)
        local term = ev.data.terminal_window
        if term and vim.api.nvim_win_is_valid(term) then
          vim.api.nvim_win_set_width(term, math.floor(vim.o.columns * 0.20))
        end
      end,
    })
    
    vim.api.nvim_create_autocmd("User", {
      pattern = "ClaudeCodeDiffClosed",
      callback = function(ev)
        -- restore your preferred idle layout here
      end,
    })
  6. Set up the development environment with mise

    main

    The project uses mise to provision the toolchain (Neovim, LuaJIT, formatters, and test runners). After installing mise, use the following commands to install tools and build the Lua test rocks into ./.luarocks:

    1. Install all tools: mise install
    2. Build Lua test rocks: mise run setup

    To ensure tools are on your PATH, add eval "$(mise activate bash)" (or your specific shell's activation command) to your shell configuration file.

    Common development tasks available via mise:

    • mise run all: Runs format, lint, and test.
    • mise run test: Runs the test suite.
    • mise run check: Runs linting.
    • mise run format: Formats code.
    mise install      # install all tools (builds Lua/LuaJIT from source)
    mise run setup     # build the Lua test rocks (busted/luacheck/luacov) into ./.luarocks
  7. Run tests and linting

    main

    Use mise to run the full test suite or linting. To run a specific unit test file, execute Neovim in headless mode using the provided tests/minimal_init.lua.

    • Run all tests: mise run test
    • Run linting: mise run check
    • Format code: mise run format
    • Run specific test file: nvim --headless -u tests/minimal_init.lua -c "lua require('tests.unit.config_spec')"
    # Run all tests
    mise run test
    
    # Run specific test file
    nvim --headless -u tests/minimal_init.lua -c "lua require('tests.unit.config_spec')"
    
    # Run linting
    mise run check
    
    # Format code
    mise run format
  8. Manage Claude Code diffs

    main

    When Claude proposes changes, the plugin opens a native Neovim diff view. You can edit suggestions before accepting them.

    • To Accept: Save the buffer with :w or use the command :ClaudeCodeDiffAccept.
    • To Reject: Quit the buffer with :q or use the command :ClaudeCodeDiffDeny.

    If you resolve diffs remotely while the session is still connected, run :ClaudeCodeCloseAllDiffs to clear leftover pending proposals. This command leaves accepted/saved diffs (those written to disk) untouched.

  9. Use the native Diff System for proposed changes

    main

    The plugin uses a native Neovim diff implementation to show proposed changes. It creates a temporary file with the new content and opens a diff view in the current tab. This allows you to review and accept or reject changes using standard Neovim diff commands or custom keymaps.

    -- Custom keymaps for diff mode
    vim.keymap.set("n", "<leader>da", accept_all_changes)
    vim.keymap.set("n", "<leader>dq", exit_diff_mode)
  10. Install claudecode.nvim using lazy.nvim

    main

    Install claudecode.nvim with folke/snacks.nvim as a dependency. The following configuration uses lazy-loading via cmd and keys to ensure the plugin only loads when needed. Including the cmd list ensures that :ClaudeCode commands are available even before any keymaps are pressed.

    {
      "coder/claudecode.nvim",
      dependencies = { "folke/snacks.nvim" },
      config = true,
      -- `cmd` lets lazy.nvim create command stubs that load the plugin on first use,
      cmd = {
        "ClaudeCode",
        "ClaudeCodeFocus",
        "ClaudeCodeSelectModel",
        "ClaudeCodeAdd",
        "ClaudeCodeSend",
        "ClaudeCodeTreeAdd",
        "ClaudeCodeStatus",
        "ClaudeCodeStart",
        "ClaudeCodeStop",
        "ClaudeCodeOpen",
        "ClaudeCodeClose",
        "ClaudeCodeDiffAccept",
        "ClaudeCodeDiffDeny",
        "ClaudeCodeCloseAllDiffs",
      },
      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", "minifiles", "netrw", "snacks_picker_list" },
        },
        -- Diff management
        { "<leader>aa", "<cmd>ClaudeCodeDiffAccept<cr>", desc = "Accept diff" },
        { "<leader>ad", "<cmd>ClaudeCodeDiffDeny<cr>", desc = "Deny diff" },
      },
    }
  11. Implement thread-safe Neovim API calls in async contexts

    main

    Because the WebSocket server operates asynchronously, all calls to Neovim's core API (vim.*) from within async callbacks or event loops must be wrapped in vim.schedule() to ensure thread safety.

    client:on("message", function(data)
      vim.schedule(function()
        -- Safe to use vim.* APIs here
      end)
    end)
  12. Perform integration testing with fixtures

    main

    When adding support for new integrations (like file explorers or terminals), you must provide a fixture configuration in fixtures/[integration-name]/.

    A valid fixture must include:

    • A complete Neovim configuration.
    • Plugin dependencies and setup.
    • A dev-claudecode.lua file containing development keybindings.

    To test using these fixtures, source the helper aliases and use the vv command:

    1. source fixtures/nvim-aliases.sh
    2. vv <integration-name> (e.g., vv nvim-tree, vv oil, vv mini-files, or vv netrw)

    Other helper commands:

    • list-configs: Lists available configurations.
    • repro: Creates a minimal reproduction environment by copying fixtures/repro/example to /tmp.
    # Source fixture aliases
    source fixtures/nvim-aliases.sh
    
    # Test with specific integration
    vv nvim-tree  # Start Neovim with nvim-tree configuration
    vv oil        # Start Neovim with oil.nvim configuration
    vv mini-files # Start Neovim with mini.files configuration
    vv netrw      # Start Neovim with built-in netrw configuration
    
    # List available configurations
    list-configs
    
    # Minimal repro environment
    repro