Neovim integration for wrt, a Git worktree helper
built for parallel and agentic development.
wrt owns more than git worktree does: reserved port blocks, generated .env / .wrt.env
files and Supabase allocations, all recorded in <managed-root>/.git/.wrt/state.json. This plugin
drives it from inside the editor, and — crucially — moves your open buffers with you when you
switch worktrees.
- Pick and switch worktrees, with dirty markers and a git preview.
- Open buffers follow you to the same relative path in the target worktree, keeping window layout and cursor position. Missing files close; unsaved buffers are left alone.
wrt envis pushed intovim.envon every switch, so terminals, tasks and LSP children inherit that worktree's port block.- Create, remove, prune, housekeeping,
root status, database tasks, a worktree shell, and a:Wrtpassthrough for everything else.
-
Neovim >= 0.11
-
The
wrtCLI. There are no tagged releases yet, so install from source:cargo install --locked --git https://github.com/pascalporedda/wrt-cli
It lands in
~/.cargo/bin/wrt, which the plugin finds even if it is not on$PATH. -
git
Optional, for a nicer picker — the plugin detects whichever you have and falls back to
vim.ui.select when you have none:
- snacks.nvim (
Snacks.picker) - telescope.nvim
- fzf-lua
Run :checkhealth wrt after installing. It reports the resolved wrt path and whether the binary
runs, the git version, whether the current directory is inside a managed root, the state.json
version compatibility, the picker backend that will be used, and which optional dependencies are
present.
Neovim discovers health checks from the runtimepath, and lazy.nvim only adds a plugin to the runtimepath once it loads. If you lazy-load this plugin with
cmd/keys,:checkhealth wrtreports "no healthcheck found" until you have used it once in that session. Install without lazy triggers (as above) to have it always available.
With lazy.nvim:
{ "pascalporedda/wrt.nvim", opts = {} }That gives you :Wrt and the full Lua API but no keymaps — a published plugin should not claim
a prefix you may already use. To get the documented set under <leader>W:
{
"pascalporedda/wrt.nvim",
opts = { keymaps = true },
}Lazy-loading works without eager setup:
{
"pascalporedda/wrt.nvim",
cmd = "Wrt",
keys = {
{ "<leader>Ww", function() require("wrt").pick() end, desc = "Worktrees (switch)" },
{ "<leader>Wn", function() require("wrt").create() end, desc = "New worktree" },
},
opts = {},
}keymaps = true defines these under keymap_prefix (default <leader>W):
| Key | Action | Description |
|---|---|---|
<leader>Ww / <leader>WW |
pick |
Worktrees (switch) |
<leader>Wn |
create |
New worktree |
<leader>Wx |
remove_current |
Remove current worktree |
<leader>Wm |
goto_main |
Go to main worktree |
<leader>Wp |
prune |
Prune missing worktrees |
<leader>Wh |
housekeeping_dry |
Housekeeping (dry run) |
<leader>WH |
housekeeping_apply |
Housekeeping (apply) |
<leader>Ws |
status |
Root status |
<leader>Wi |
discover |
Discover .wrt.json (codex) |
<leader>Wc |
clone |
Clone a new managed root |
<leader>We |
apply_env_current |
Apply worktree env |
<leader>Wt |
shell_current |
Shell in worktree |
<leader>Wr |
run_current |
Run command in worktree |
<leader>Wd |
db_current |
Database task |
Override individual entries by suffix; false removes one:
opts = {
keymaps = {
x = false, -- drop the remove mapping
z = "goto_main", -- add <leader>Wz
},
keymap_prefix = "<leader>gw",
}Available with the snacks and telescope backends:
| Key | Action |
|---|---|
<cr> |
Switch to the worktree |
<c-x> |
Remove worktree |
<a-n> |
New worktree |
<a-e> |
Apply that worktree's wrt env |
<a-t> |
Shell in the worktree |
<a-b> |
Database task |
<a-y> |
Yank the path |
<c-l> |
Reload (snacks only) |
With snacks these also work from normal mode in the list: x, n, e, t, b, y, r.
Defaults in full:
require("wrt").setup({
-- Absolute path to the wrt binary. nil = $PATH, then ~/.cargo/bin/wrt.
bin = nil,
-- "auto" probes snacks -> telescope -> fzf-lua -> select.
picker = "auto", -- "auto"|"snacks"|"telescope"|"fzf-lua"|"select"
-- false = define nothing. true = the table above. table = per-suffix overrides.
keymaps = false,
keymap_prefix = "<leader>W",
which_key = true,
which_key_group = "worktree (wrt)",
switch = {
migrate_buffers = true, -- move open buffers into the target worktree
clear_jumps = true,
apply_env = true, -- push `wrt env` into vim.env
notify = true, -- report the worktree and migration counts
},
env = {
notify = false, -- notify on every automatic env application
},
create = {
switch_after = true, -- switch into the new worktree when `wrt new` exits 0
},
notify = {
enabled = true,
title = "wrt",
},
-- Floating terminal geometry. Fractions are relative to the editor.
terminal = {
border = "rounded",
width = 0.85,
height = 0.8,
backdrop = 60, -- snacks only
title_pos = "center",
},
git = {
log_count = 12, -- commits in the picker preview
},
}):Wrt with no arguments opens the picker. With arguments it runs any wrt command in a floating
terminal at the managed root, which is always a valid working directory for the CLI:
:Wrt ls
:Wrt new feat/login --from origin/main
:Wrt db feature-demo reset --yes
:Wrt root statusCompletion offers subcommands first, then worktree names.
Arguments are tokenized like a shell, so quotes and escapes work as typed.
local wrt = require("wrt")
wrt.pick(opts) -- open the picker
wrt.switch("feature-demo") -- switch by name or by item
wrt.create() -- prompt, then `wrt new`
wrt.create_named(name, flags)
wrt.remove(target) -- nil = the worktree containing the cwd
wrt.list(root) -- wrt.Worktree[], main first then alphabetical
wrt.current() -- wrt.Worktree|nil
wrt.main()
wrt.goto_main()
wrt.root(path) -- wrt.Root|nil, string|nil (reason)
wrt.slug(name) -- mirrors the CLI's slug()
wrt.apply_env(target, opts)
wrt.restore_env()
wrt.prune()
wrt.housekeeping(apply)
wrt.status() -- `wrt root status`
wrt.discover() -- `wrt init`
wrt.clone()
wrt.shell(target)
wrt.run(target)
wrt.db(target)Names are slugged for you, so wrt.switch("Feature/Demo") finds feature-demo.
Switching fires a User autocommand:
vim.api.nvim_create_autocmd("User", {
pattern = "WrtSwitch",
callback = function(args)
-- args.data = { name = "feature-demo", path = "/…/feature-demo", branch = "feature/demo" }
end,
})It fires after buffer migration, the :cd and :clearjumps, but wrt env is applied
asynchronously, so those variables may land in vim.env shortly afterwards.
- Multi-word names need no quoting. In the
wrt newprompt everything before the first-flagis the name, soAgent 07: retry queue --branch agent/retryis one positional plus two flags. Note that a name containing a colon produces an illegal branch name, so pass--branchas in that example. mainis an allocation key, not a directory name. The primary checkout is tracked asmaineven when its directory is named after a different default branch, and it cannot be removed. Removing the worktree you are standing in switches to main first.wrt envmay legitimately fail when a Supabase-bound worktree's stack is not running. That happens routinely while switching, so it is silent unless you ask for notifications.- Environment restore, not delete. Values that
wrt envoverwrites are remembered and put back on the next switch, and a generation counter stops a slowwrt envfrom clobbering a newer switch.
Point lazy.nvim at a checkout instead of GitHub:
{
dir = "~/src/wrt.nvim", -- or: "pascalporedda/wrt.nvim", dev = true
opts = {},
}With dev = true, lazy.nvim resolves the plugin under your dev.path (~/projects by default);
set dev = { path = "~/src" } in your lazy.nvim setup to match.
The suite runs headless against a real wrt binary and throwaway managed roots that it
creates and deletes itself.
make test-bare # core Neovim only, proves the vim.ui.select fallback works
make test-all # clones snacks + telescope into .tests/ and exercises those adapters
make lint # stylua --checkMIT