Pairup.nvim
AI Pair Programming in Neovim
Install / Use
/learn @Piotr1215/Pairup.nvimREADME
pairup.nvim
Inline AI pair programming for Neovim.
🔺 Breaking Changes in v4.0 🔺
This version removes the overlay system, sessions, RPC, and marker-based suggestions to focus on one thing: simple inline editing with
cc:/uu:markers. See v4-architecture.md for details.Why? Less complexity, more reliability. Claude edits files directly — no parsing, no overlays, no state management. Just write
cc:, save, and Claude handles it.Need the old features? Use
git checkout legacy-v3orgit checkout v3.0.0.
How It Works
Write cc:, cc!:, or ccp: markers anywhere in your code, save, and Claude edits the file directly.
sequenceDiagram
autonumber
participant U as User
participant P as Plugin (Neovim)
participant C as Claude CLI
participant H as Hook
U->>P: gCC (insert cc: marker)
P->>U: Signs, colors, statusbar
U->>P: :w (save file)
P->>C: Send prompt + file path
C->>H: TodoWrite (in_progress)
H->>P: JSON
P->>U: [C:0/1] + extmark
C->>P: Edit file
P->>U: Flash changed lines
C->>H: TodoWrite (completed)
H->>P: JSON
P->>U: [C:ready], clear extmark
-- cc: add logging
-- uu: Use print, vim.notify, or a logging library?
function get_user(id)
return db.users[id].name
end
Save → Claude reads the file → executes the instruction → removes the marker.
See prompt.md for the full prompt.
Neovim-Native Operators
Three separate operators for each marker type. Each works with any motion/text-object:
[!NOTE] These keybindings are only active when pairup is loaded. They won't conflict with other plugins or built-in vim mappings when pairup is not in use.
| Marker | Operator | Line | Visual |
|--------|----------|------|--------|
| cc: | gC{motion} | gCC | gC |
| cc!: | g!{motion} | g!! | g! |
| ccp: | g?{motion} | g?? | g? |
| Motion | Scope Hint | Example Output |
|--------|------------|----------------|
| iw/aw | <word> | cc: <word> |
| iW/aW | <WORD> | cc: <WORD> |
| is/as | <sentence> | cc: <sentence> |
| ip/ap | <paragraph> | cc: <paragraph> |
| if/af | <function> | cc: <function> |
| ic/ac | <codeblock> | cc: <codeblock> |
| F | <file> | gCF → cc: <file> |
| double-tap | <line> | g!! → cc!: <line> |
| visual | <selection> | captures text |
Example: Select "controller configuration" and press gC:
// cc: <selection> controller configuration <-
// Config holds the controller configuration
Signs
Markers show in the gutter:
- (yellow) —
cc:command /cc!:constitution /ccp:plan - (blue) —
uu:question marker
Plan Marker (Review Before Apply)
Use ccp: when you want to review Claude's changes before applying them:
-- ccp: add error handling
function process(data)
return data.value
end
Claude wraps changes in conflict markers:
<<<<<<< CURRENT
function process(data)
return data.value
end
=======
function process(data)
if not data then
return nil, "missing data"
end
return data.value
end
>>>>>>> PROPOSED
Accept/Reject: Position cursor in the section you want to keep, then :Pairup accept (or <Plug>(pairup-accept)):
- Cursor in CURRENT → keep original (reject proposal)
- Cursor in PROPOSED → keep Claude's change (accept proposal)
Navigation: Use <Plug>(pairup-proposal-next) and <Plug>(pairup-proposal-prev) to jump between proposals.
Edit view: Use <Plug>(pairup-proposal-edit) to open a floating editor for the proposal at cursor. The editor shows only the PROPOSED content (editable), with CURRENT info and reason as virtual text (non-editable). A subtle backdrop dims the background for focus.
| Key | Action |
|-----|--------|
| :w | Sync edits to buffer |
| q | Close editor |
| ga | Accept proposal |
| gd | Open diff view |
Diff view: Use <Plug>(pairup-conflict-diff) to open a side-by-side diff in a new tab. Press ga to accept, ge to switch to edit view, q to close. Note: Opens with diffopt+=algorithm:patience,indent-heuristic for cleaner diffs.

Mix and match: Add cc: inside PROPOSED to refine before accepting:
<<<<<<< CURRENT
function process(data)
return data.value
end
=======
-- cc: also add logging
function process(data)
if not data then
return nil, "missing data"
end
return data.value
end
>>>>>>> PROPOSED
Save → Claude refines the PROPOSED section → review again → accept when satisfied.
Constitution Marker
Use cc!: when you want Claude to both execute an instruction AND extract the underlying rule into CLAUDE.md:
-- cc!: use snake_case for all variable names
local myVar = 1
Claude will rename myVar to my_var and add "use snake_case for variables" to your project's CLAUDE.md.
Questions
When Claude needs more information, it adds uu: and you can continue discussion by appending cc: in response.
-- cc: add error handling here
-- uu: Should I use pcall or assert?
function process(data)
return data.value
end
Installation
Key bindings are optional — the plugin works with :Pairup commands alone.
-- lazy.nvim
{
"Piotr1215/pairup.nvim",
cmd = { "Pairup" },
keys = {
{ "<leader>cc", "<cmd>Pairup start<cr>", desc = "Start Claude" },
{ "<leader>ct", "<cmd>Pairup toggle<cr>", desc = "Toggle terminal" },
{ "<leader>cq", "<cmd>Pairup markers user<cr>", desc = "Show uu: questions" },
{ "<leader>cC", "<cmd>Pairup markers claude<cr>", desc = "Show cc: markers" },
{ "<leader>cx", "<cmd>Pairup stop<cr>", desc = "Stop Claude" },
},
config = function()
require("pairup").setup()
-- Default works out of the box. Override only if needed:
-- require("pairup").setup({
-- providers = {
-- claude = { cmd = "claude --permission-mode plan" },
-- },
-- })
end,
}
Commands
:Pairup <subcommand>
| Command | Description |
|---------|-------------|
| start | Start Claude (hidden terminal) |
| stop | Stop Claude |
| toggle | Show/hide terminal |
| say <msg> | Send message to Claude |
| markers user | Show uu: in quickfix |
| markers claude | Show cc:/cc!:/ccp: in quickfix |
| markers proposals | Show PROPOSED sections in quickfix |
| inline | Manual cc: trigger |
| diff | Send git diff to Claude |
| lsp | Send LSP diagnostics to Claude |
| suspend | Pause auto-processing (indicator turns red) |
| accept | Accept conflict section at cursor |
| edit | Open floating editor for proposal |
| next | Jump to next proposal |
| prev | Jump to previous proposal |
Status Indicator
Automatically injected into lualine (or native statusline if no lualine). No config needed.
[C]— Claude running[C:pending]— Waiting for Claude[C:2/5]— Todo progress (2 of 5 tasks done)[C:ready]— All tasks complete
Progress Tracking
Track Claude Code's todo list progress via a PostToolUse hook. When Claude uses TodoWrite to plan multi-step tasks, the statusline shows live progress like [C:2/5] and virtual text appears below your cursor showing the current task. Copy the hook script:
cp /path/to/pairup.nvim/scripts/__pairup_todo_hook.sh ~/.claude/scripts/
chmod +x ~/.claude/scripts/__pairup_todo_hook.sh
Add to ~/.claude/settings.json:
{
"hooks": {
"PostToolUse": [
{
"matcher": "TodoWrite",
"hooks": [{"type": "command", "command": "$HOME/.claude/scripts/__pairup_todo_hook.sh"}]
}
]
}
}
Enable in pairup:
require("pairup").setup({
progress = {
enabled = true,
},
})
Manual setup (only if you disable auto-inject or use a custom statusline plugin):
-- Disable auto-inject
require("pairup").setup({ statusline = { auto_inject = false } })
-- Add to native statusline manually
vim.o.statusline = '%f %m%=%{g:pairup_indicator} %l:%c'
Configuration
Default: The --permission-mode acceptEdits flag is included by default. This allows Claude to edit files without prompting for confirmation on each change, which is required for the inline editing workflow to function smoothly.
All settings below are defaults. You only need to include values you want to change:
require("pairup").setup({
provider = "claude",
providers = {
claude = {
cmd = "claude --permission-mode acceptEdits",
},
},
git = {
enabled = true,
diff_context_lines = 10,
},
terminal = {
split_position = "left",
split_width = 0.4,
auto_insert = false,
auto_scroll = true,
},
auto_refresh = {
enabled = true,
interval_ms = 500,
},
inline = {
markers = {
command = "cc:",
question = "uu:",
constitution = "cc!:",
plan = "ccp:",
},
quickfix = true,
auto_process = true, -- Auto-send to Claude on save (false = manual :Pairup inline)
},
statusline = {
auto_inject = true,
},
progress = {
enabled = false,
session_id = nil, -- auto-detects from /tmp/pairup-todo-*.json
},
flash = {
scroll_to_changes = false,
},
operator = {
command_key = "gC",
constitution_key = "g!",
plan_key = "g?",
},
})
Highlight Groups
Customizable highlight groups (respects light/dark background by default):
-- In your colorscheme or after/plugin/colors.lua:
vim.api.nvim_set_hl(0, 'PairupMarkerCC', { bg = '#your_color' }) -- cc: marker line
vim.api.nvim_set_hl(0, 'PairupMarkerUU', { bg = '#your_color' }) -- uu: marker line
vim.api.nvim_set_hl(0, 'PairupFlash', { bg = '#your_color' }) -- changed lines flash
vim.api.nvim_set_hl(0, 'PairupBackdrop', { bg = '#000000' }) -- edit float backdrop
Plug Mappings
Available <Plug> mappings for custom keybindings:
vim.keymap.set('n',
