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, OpenCode, GitHub Copilot CLI, and OpenAI Codex CLI as backends.
- Features
- Requirements
- Installation
- Quick Start
- How it works
- Configuration
- Commands
- Keymaps
- 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, OpenCode, GitHub Copilot CLI, and OpenAI Codex CLI
- No Python dependency — file transformations use
nvim --headless -l
- Neovim >= 0.10
For Claude Code backend:
- Claude Code CLI with hooks support
- jq — for JSON parsing in hook scripts
For OpenCode backend:
- OpenCode >= 1.3.0
For GitHub Copilot CLI backend:
- GitHub Copilot CLI (generally available since Feb 2026)
For OpenAI Codex CLI backend:
- OpenAI Codex CLI (recent enough to support
apply_patchPreToolUse hooks; older builds only fired hooks forBash) - jq — for hook payload translation
{
"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
- Install the plugin and call
setup() - Open a project in Neovim
- Run
:CodePreviewInstallCopilotCliHooks— writes.github/hooks/code-preview.json - Start Copilot CLI in the project directory
- Ask Copilot 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
Note: Copilot CLI does not fire post-tool hooks on rejection, so rejected diffs remain open until you dismiss them (same as Claude Code).
-
Install the plugin and call
setup() -
Open a project in Neovim
-
Run
:CodePreviewInstallCodexCliHooks— writes.codex/hooks.json -
Codex requires a feature flag to enable hooks, and the diff-preview workflow only makes sense when Codex asks before applying edits. Create or edit
.codex/config.toml(project-local) or~/.codex/config.toml(global) and add:approval_policy = "on-request" sandbox_mode = "read-only" [features] codex_hooks = true
approval_policy = "on-request"andsandbox_mode = "read-only"ensure Codex prompts you before every edit, so the diff preview has time to open and you have time to review. Without them, Codex may apply changes without prompting and the preview window will never block on your decision.The installer warns you if
codex_hooksis missing. You can re-check at any time with:CodePreviewStatusor:checkhealth code-preview, which both report whether the feature flag is detected. -
Start Codex CLI in the project directory
-
Ask Codex 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
Note: Today's Codex models route all file edits through the
apply_patchtool. New file creation that Codex performs via shell redirection (e.g.printf … > foo.txt) is not previewed — onlyapply_patchand edits via the dedicatedEdit/Writetools (when emitted) are.
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/.
GitHub Copilot CLI uses shell-based hooks (preToolUse/postToolUse) configured in .github/hooks/code-preview.json. The adapter translates Copilot's tool vocabulary (apply_patch, edit, create, bash) into the same normalized format used by the other backends.
OpenAI Codex CLI uses shell-based hooks (PreToolUse/PostToolUse) configured in .codex/hooks.json, gated by codex_hooks = true under [features] in .codex/config.toml. The adapter passes Bash through and rewrites apply_patch (whose patch text lives in tool_input.command) into the canonical ApplyPatch shape with tool_input.patch_text.
All backends communicate with Neovim via RPC (nvim --server <socket> --remote-send).
All options with defaults:
require("code-preview").setup({
debug = false, -- enable debug logging to stdpath("log")/code-preview.log
diff = {
layout = "tab", -- "tab" (new tab) | "vsplit" (current tab) | "inline" (GitHub-style)
labels = { current = "CURRENT", proposed = "PROPOSED" },
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 |
:CodePreviewInstallCopilotCliHooks |
Install Copilot CLI hooks to .github/hooks/code-preview.json |
:CodePreviewUninstallCopilotCliHooks |
Remove Copilot CLI hooks |
:CodePreviewInstallCodexCliHooks |
Install Codex CLI hooks to .codex/hooks.json |
:CodePreviewUninstallCodexCliHooks |
Remove Codex CLI hooks |
: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 (all backends) |
Migrating? The old
:ClaudePreview*commands still work but show a deprecation warning. They will be removed in a future release.
| Key | Scope | Description |
|---|---|---|
<leader>dq |
global | Close the diff (same as :CodePreviewCloseDiff) |
]c |
inline diff buffer | Jump to next change |
[c |
inline diff buffer | Jump to previous change |
All defaults are configurable via setup():
require("code-preview").setup({
keys = {
next_change = "]c", -- inline diff: next change
prev_change = "[c", -- inline diff: previous change
close_all = "<leader>dq", -- close diff and clear indicators
},
})Set any entry to false to skip that binding, or keys = false to skip them all. A <Plug>(CodePreviewCloseAll) mapping is always defined, so you can bind it yourself even with keys = false:
vim.keymap.set("n", "<leader>x", "<Plug>(CodePreviewCloseAll)")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()
│ ├── log.lua opt-in debug logging
│ ├── changes.lua change status registry (modified/created/deleted)
│ ├── neo_tree.lua neo-tree integration (icons, virtual nodes, reveal)
│ ├── health.lua :checkhealth (all backends)
│ └── backends/
│ ├── claudecode.lua Claude Code hook install/uninstall
│ ├── opencode.lua OpenCode plugin install/uninstall
│ ├── copilot.lua GitHub Copilot CLI hook install/uninstall
│ └── codex.lua OpenAI Codex CLI hook 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
│ └── apply-patch.lua ApplyPatch transformer (custom patch format)
├── 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
│ ├── copilot/ GitHub Copilot CLI adapter
│ │ ├── code-preview-diff.sh preToolUse hook — translates Copilot JSON → core
│ │ └── code-close-diff.sh postToolUse hook — same for close
│ └── codex/ OpenAI Codex CLI adapter
│ ├── code-preview-diff.sh PreToolUse hook — translates Codex JSON → core
│ └── code-close-diff.sh PostToolUse hook — same for close
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 only
./tests/run.sh backends/copilot # GitHub Copilot CLI backend only
./tests/run.sh backends/codex # OpenAI Codex CLI 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 - Enable debug logging (
debug = truein setup) and check~/.local/state/nvim/code-preview.log - 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
Codex CLI hooks not firing
- Run
:CodePreviewInstallCodexCliHooksin the project root - Confirm
.codex/config.tomlcontains[features]withcodex_hooks = true(without it, Codex ignoreshooks.jsonsilently) - Update Codex if needed — older versions only fired hooks for
Bash, notapply_patch - Run
:CodePreviewStatusand:checkhealth code-previewto verify install state and the feature flag
Copilot CLI hooks not firing
- Run
:CodePreviewInstallCopilotCliHooksin the project root - Verify
.github/hooks/code-preview.jsonexists - Ensure
jqis in PATH - Restart Copilot CLI (hooks are loaded at session start)
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




