A Neovim plugin that shows a diff preview before your AI coding agent applies any file change — letting you review exactly what's changing before accepting.
Supports Claude Code and OpenCode as backends.
- Features
- Requirements
- Installation
- Quick Start
- How it works
- Configuration
- Commands
- Diff Layouts
- Neo-tree Integration
- Testing
- Troubleshooting
- Diff preview — side-by-side or inline diff opens in Neovim before any file is written
- Multiple layouts — tab, vsplit, or GitHub-style inline diff with syntax highlighting
- Neo-tree integration — file tree indicators show which files are being modified, created, or deleted
- Multi-backend — works with Claude Code CLI and OpenCode
- No Python dependency — file transformations use
nvim --headless -l
- Neovim >= 0.9
For Claude Code backend:
- Claude Code CLI with hooks support
- jq — for JSON parsing in hook scripts
For OpenCode backend:
- OpenCode >= 1.3.0
{
"Cannon07/code-preview.nvim",
config = function()
require("code-preview").setup()
end,
}vim.opt.rtp:prepend("/path/to/code-preview.nvim")
require("code-preview").setup()- Install the plugin and call
setup() - Open a project in Neovim
- Run
:CodePreviewInstallClaudeCodeHooks— writes hooks to.claude/settings.local.json - Restart Claude Code CLI in the project directory
- Ask Claude to edit a file — a diff opens automatically in Neovim
- Accept/reject in the CLI; the diff closes automatically on accept
- If rejected, press
<leader>dqto close the diff manually
- Install the plugin and call
setup() - Open a project in Neovim
- Run
:CodePreviewInstallOpenCodeHooks— copies the plugin to.opencode/plugins/ - Ensure your OpenCode config (
~/.config/opencode/opencode.json) has permission prompts enabled:{ "permission": { "edit": "ask", "bash": "ask" } } - Start OpenCode in the project directory
- Ask OpenCode to edit a file — a diff opens automatically in Neovim
- Accept/reject in OpenCode; the diff closes automatically on accept
- If rejected, press
<leader>dqto close the diff manually
AI Agent (terminal) Neovim
| |
Proposes an Edit |
| |
Hook/plugin fires ──→ compute diff ──→ RPC → show_diff()
| | (side-by-side or inline)
CLI: "Accept? (y/n)" |
| User reviews diff
User accepts/rejects |
| |
Post hook fires ────→ cleanup ─────→ RPC → close_diff()
Claude Code uses shell-based hooks (PreToolUse/PostToolUse) configured in .claude/settings.local.json.
OpenCode uses a TypeScript plugin (tool.execute.before/tool.execute.after) loaded from .opencode/plugins/.
Both backends communicate with Neovim via RPC (nvim --server <socket> --remote-send).
All options with defaults:
require("code-preview").setup({
diff = {
layout = "tab", -- "tab" (new tab) | "vsplit" (current tab) | "inline" (GitHub-style)
labels = { current = "CURRENT", proposed = "PROPOSED" },
auto_close = true, -- close diff after accept
equalize = true, -- 50/50 split widths (tab/vsplit only)
full_file = true, -- show full file, not just diff hunks (tab/vsplit only)
visible_only = false, -- skip diffs for files not open in any Neovim buffer
defer_claude_permissions = false, -- for Claude Code: let its own settings decide, don't prompt
},
highlights = {
current = { -- CURRENT (original) side — tab/vsplit layouts
DiffAdd = { bg = "#4c2e2e" },
DiffDelete = { bg = "#4c2e2e" },
DiffChange = { bg = "#4c3a2e" },
DiffText = { bg = "#5c3030" },
},
proposed = { -- PROPOSED side — tab/vsplit layouts
DiffAdd = { bg = "#2e4c2e" },
DiffDelete = { bg = "#4c2e2e" },
DiffChange = { bg = "#2e3c4c" },
DiffText = { bg = "#3e5c3e" },
},
inline = { -- inline layout
added = { bg = "#2e4c2e" }, -- added line background
removed = { bg = "#4c2e2e" }, -- removed line background
added_text = { bg = "#3a6e3a" }, -- changed characters (added)
removed_text = { bg = "#6e3a3a" }, -- changed characters (removed)
},
},
})| Command | Description |
|---|---|
:CodePreviewInstallClaudeCodeHooks |
Install Claude Code hooks to .claude/settings.local.json |
:CodePreviewUninstallClaudeCodeHooks |
Remove Claude Code hooks (leaves other hooks intact) |
:CodePreviewInstallOpenCodeHooks |
Install OpenCode plugin to .opencode/plugins/ |
:CodePreviewUninstallOpenCodeHooks |
Remove OpenCode plugin |
:CodePreviewCloseDiff |
Manually close the diff (use after rejecting a change) |
:CodePreviewStatus |
Show socket path, hook status, and dependency check |
:CodePreviewToggleVisibleOnly |
Toggle visible_only — show diffs only for open buffers |
:checkhealth code-preview |
Full health check (both backends) |
Migrating? The old
:ClaudePreview*commands still work but show a deprecation warning. They will be removed in a future release.
| Key | Description |
|---|---|
<leader>dq |
Close the diff (same as :CodePreviewCloseDiff) |
code-preview supports three diff layouts, configured via diff.layout:
| Layout | Description |
|---|---|
"tab" (default) |
Side-by-side diff in a new tab — CURRENT on the left, PROPOSED on the right |
"vsplit" |
Side-by-side diff as a vertical split in the current tab |
"inline" |
GitHub-style unified diff in a single buffer with syntax highlighting preserved |
- Syntax highlighting — the file's language highlighting is preserved
- Character-level diffs — changed portions within a line are highlighted with a brighter background
- Sign column —
+/-signs indicate added/removed lines - Navigation —
]c/[cto jump between changes
To use inline diff:
require("code-preview").setup({
diff = { layout = "inline" },
})If you use neo-tree.nvim, code-preview will automatically decorate your file tree with visual indicators when changes are proposed. No extra configuration is required — it works out of the box.
| Status | Icon | Name Color | Description |
|---|---|---|---|
| Modified | | Orange | An existing file is being edited |
| Created | | Cyan + italic | A new file is being created (shown as a virtual node) |
| Deleted | | Red + strikethrough | A file is being deleted via rm |
Additional behaviors:
- Auto-reveal — the tree expands to highlight the changed file
- Virtual nodes — new files/directories appear in the tree before they exist on disk
- Clean focus — git status, diagnostics, and modified indicators are temporarily hidden while changes are pending
- Auto-cleanup — all indicators clear when you accept, reject, or press
<leader>dq
All neo-tree options with defaults:
require("code-preview").setup({
neo_tree = {
enabled = true, -- set false to disable neo-tree integration
reveal = true, -- auto-reveal changed files in the tree
reveal_root = "cwd", -- "cwd" (current working dir) or "git" (git root)
position = "right", -- neo-tree window position: "left", "right", "float"
symbols = {
modified = "",
created = "",
deleted = "",
},
highlights = {
modified = { fg = "#e8a838", bold = true },
created = { fg = "#56c8d8", bold = true },
deleted = { fg = "#e06c75", bold = true, strikethrough = true },
},
},
})Note: Neo-tree is a soft dependency. If neo-tree is not installed, the plugin works exactly as before — only the diff preview.
code-preview.nvim/
├── lua/code-preview/
│ ├── init.lua setup(), config, commands
│ ├── diff.lua show_diff(), close_diff()
│ ├── changes.lua change status registry (modified/created/deleted)
│ ├── neo_tree.lua neo-tree integration (icons, virtual nodes, reveal)
│ ├── health.lua :checkhealth (both backends)
│ └── backends/
│ ├── claudecode.lua Claude Code hook install/uninstall
│ └── opencode.lua OpenCode plugin install/uninstall
├── bin/ Shared core scripts
│ ├── core-pre-tool.sh Unified PreToolUse logic
│ ├── core-post-tool.sh Unified PostToolUse logic
│ ├── nvim-socket.sh Neovim socket discovery
│ ├── nvim-send.sh RPC send helper
│ ├── apply-edit.lua Single Edit transformer
│ └── apply-multi-edit.lua MultiEdit transformer
├── backends/
│ ├── claudecode/ Claude Code adapter
│ │ ├── code-preview-diff.sh PreToolUse hook entry point
│ │ └── code-close-diff.sh PostToolUse hook entry point
│ └── opencode/ OpenCode adapter
│ ├── index.ts tool.execute.before/after hooks
│ ├── package.json
│ └── tsconfig.json
The test suite uses plenary.nvim for core plugin tests and shell scripts for backend integration tests. CI runs on both Ubuntu and macOS.
./tests/run.sh # all tests (plugin + backends)
./tests/run.sh plugin # core plugin tests only (plenary busted)
./tests/run.sh backends # all backend integration tests
./tests/run.sh backends/claudecode # Claude Code backend only
./tests/run.sh backends/opencode # OpenCode backend onlyDependencies: Neovim >= 0.10, jq, bun (for OpenCode tests). Plenary is auto-installed to deps/ on first run.
For buffers to auto-reload after a file is written, add this to your Neovim config:
vim.o.autoread = true
vim.api.nvim_create_autocmd({ "FocusGained", "BufEnter", "CursorHold" }, {
command = "checktime",
})Diff doesn't open
- Run
:CodePreviewStatus— check thatNeovim socketis found - Run
:checkhealth code-preview— check for missing dependencies - Restart the CLI agent after installing hooks (hooks are read at startup)
Claude Code hooks not firing
- Run
:CodePreviewInstallClaudeCodeHooksin the project root - Verify
.claude/settings.local.jsoncontains the hook entries - Ensure
jqis in PATH - Restart Claude Code CLI
OpenCode plugin not loading
- Run
:CodePreviewInstallOpenCodeHooksin the project root - Verify
.opencode/plugins/index.tsexists - Ensure
"permission": { "edit": "ask" }is set in~/.config/opencode/opencode.json - Restart OpenCode
Diff doesn't close after rejecting
- Press
<leader>dqor run:CodePreviewCloseDiff— the post hook only fires on accept
Migrating from older versions
- Update
require("claude-preview")torequire("code-preview")in your Neovim config - Re-run
:CodePreviewInstallClaudeCodeHooksto update hook paths - The old
:ClaudePreview*commands still work but show deprecation warnings
MIT — see LICENSE


