This guide explains how diff acceptance/rejection works in claudecode.nvim and provides instructions for implementing keybindings in external plugins to communicate with Claude over MCP.
-
Claude calls
openDifftool via MCP with:old_file_path: Original file pathnew_file_path: Path for naming the new buffernew_file_contents: The proposed changestab_name: Unique identifier for this diff
-
claudecode.nvim creates a diff view and blocks (using coroutines) waiting for user action
-
User accepts or rejects the diff through:
- Accept: Pressing
:w(triggersBufWriteCmdautocmd) - Reject: Closing the buffer (triggers
BufDelete/BufUnload/BufWipeoutautocmds)
- Accept: Pressing
-
claudecode.nvim sends response back to Claude:
- Accepted:
{content: [{type: "text", text: "FILE_SAVED"}, {type: "text", text: "<file_contents>"}]} - Rejected:
{content: [{type: "text", text: "DIFF_REJECTED"}, {type: "text", text: "<tab_name>"}]}
- Accepted:
IMPORTANT: When the user accepts a diff (:w), claudecode.nvim does NOT write the file locally. Instead:
- The
BufWriteCmdautocmd prevents the actual file write (returnstrue) - claudecode.nvim sends
FILE_SAVEDresponse to Claude with the buffer contents - Claude performs the actual file write after receiving the response
- claudecode.nvim then reloads the buffer to show Claude's changes
This means you should NOT edit the file locally - only inform Claude via MCP so Claude can handle the edit properly.
When a diff buffer is created, claudecode.nvim sets buffer variables:
vim.b[buffer].claudecode_diff_tab_name = "unique_tab_name"
vim.b[buffer].claudecode_diff_new_win = window_id -- The new diff window
vim.b[buffer].claudecode_diff_target_win = window_id -- The original file window-- In your plugin's setup
local function setup_diff_keybindings()
-- Map Enter to accept diff
vim.keymap.set('n', '<CR>', function()
local bufnr = vim.api.nvim_get_current_buf()
local tab_name = vim.b[bufnr].claudecode_diff_tab_name
if not tab_name then
return -- Not a diff buffer
end
-- Get buffer contents for the response
local content_lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false)
local final_content = table.concat(content_lines, "\n")
-- Add trailing newline if buffer has one
if #content_lines > 0 and vim.api.nvim_buf_get_option(bufnr, 'eol') then
final_content = final_content .. "\n"
end
-- Send MCP response to Claude
send_mcp_response({
content = {
{ type = "text", text = "FILE_SAVED" },
{ type = "text", text = final_content }
}
})
-- Close the diff windows (optional - Claude will send close_tab)
local new_win = vim.b[bufnr].claudecode_diff_new_win
local target_win = vim.b[bufnr].claudecode_diff_target_win
if new_win and vim.api.nvim_win_is_valid(new_win) then
vim.api.nvim_win_close(new_win, true)
end
if target_win and vim.api.nvim_win_is_valid(target_win) then
vim.api.nvim_set_current_win(target_win)
vim.cmd("diffoff")
end
end, { buffer = bufnr, desc = "Accept diff changes" })
-- Map Escape to reject diff
vim.keymap.set('n', '<Esc>', function()
local bufnr = vim.api.nvim_get_current_buf()
local tab_name = vim.b[bufnr].claudecode_diff_tab_name
if not tab_name then
return -- Not a diff buffer
end
-- Send MCP response to Claude
send_mcp_response({
content = {
{ type = "text", text = "DIFF_REJECTED" },
{ type = "text", text = tab_name }
}
})
-- Close the diff windows
local new_win = vim.b[bufnr].claudecode_diff_new_win
local target_win = vim.b[bufnr].claudecode_diff_target_win
if new_win and vim.api.nvim_win_is_valid(new_win) then
vim.api.nvim_win_close(new_win, true)
end
if target_win and vim.api.nvim_win_is_valid(target_win) then
vim.api.nvim_set_current_win(target_win)
vim.cmd("diffoff")
end
end, { buffer = bufnr, desc = "Reject diff changes" })
end
-- Set up autocmd to add keybindings when diff buffers are created
vim.api.nvim_create_autocmd("BufEnter", {
callback = function()
local bufnr = vim.api.nvim_get_current_buf()
if vim.b[bufnr].claudecode_diff_tab_name then
setup_diff_keybindings()
end
end
})You need to send the response through the existing WebSocket connection to Claude:
function send_mcp_response(response_data)
-- This depends on how your plugin manages the WebSocket connection
-- You need to:
-- 1. Find the pending request ID for the openDiff call
-- 2. Send a JSON-RPC response with that ID
local response = {
jsonrpc = "2.0",
id = get_pending_diff_request_id(), -- You need to track this
result = response_data
}
-- Send via your WebSocket implementation
websocket_send(vim.json.encode(response))
end- DO NOT write the file locally - Claude handles the actual file write after receiving
FILE_SAVED - The diff operation is blocking - Claude waits for your response before continuing
- Track request IDs - You need to respond with the same ID from the original
openDiffrequest - Buffer cleanup - Claude will send a
close_tabtool call after processing the response - Navigation - After accepting, the plugin handles navigating to the changed file location
Instead of reimplementing, you could directly call claudecode.nvim's functions:
-- To accept current diff
require('claudecode.diff').accept_current_diff()
-- To reject current diff
require('claudecode.diff').deny_current_diff()These functions handle all the MCP communication internally.
- Start claudecode.nvim server:
:ClaudeCodeStart - Ask Claude to make a code change that opens a diff
- Test your keybindings:
- Press Enter → Should accept and Claude writes the file
- Press Escape → Should reject and close the diff
- Verify Claude receives the correct response and continues appropriately
- Check buffer variables:
:echo b:claudecode_diff_tab_name - Monitor WebSocket messages for proper MCP responses
- Ensure request IDs match between openDiff call and response
- Watch for Claude's follow-up
close_tabtool call after accepting