vde-worktree
v0.0.22
Published
Git worktree manager with safe defaults for humans and coding agents
Readme
vde-worktree
vde-worktree is a safe Git worktree manager designed for both humans and coding agents.
It installs two command names:
vde-worktreevw(alias)
Japanese documentation: README.ja.md
Goals
- Keep managed worktrees under a configurable root (default:
.worktree/) - Provide idempotent branch-to-worktree operations
- Prevent accidental destructive actions by default
- Expose stable JSON output for automation
- Support hook-driven customization
Requirements
- Node.js 22+
- pnpm 10+
fzf(required forcd)gh(optional, for PR-based merge status)
Install / Build
Global install:
npm install -g vde-worktreeLocal build:
pnpm install
pnpm run buildValidate locally:
pnpm run ciQuick Start
vw init
vw switch feature/foo
cd "$(vw cd)"vw cd prints the selected worktree path. It cannot change the parent shell directory by itself.
Shell Completion
Generate from command:
vw completion zsh
vw completion fishInstall to default locations:
vw completion zsh --install
vw completion fish --installInstall to custom file path:
vw completion zsh --install --path ~/.zsh/completions/_vw
vw completion fish --install --path ~/.config/fish/completions/vw.fishFor zsh, ensure completion path is loaded:
fpath=(~/.zsh/completions $fpath)
autoload -Uz compinit && compinitManaged Directories
After vw init, the tool manages:
<worktreeRoot>/(managed worktree root; default:.worktree/).vde/worktree/hooks/.vde/worktree/logs/.vde/worktree/locks/.vde/worktree/state/
init updates .git/info/exclude idempotently.
Global Behavior
- Most write commands require prior
init. - Write commands are protected by an internal repository lock.
--jsonprints exactly one JSON object to stdout.- Logs and warnings are written to stderr.
- Non-TTY unsafe overrides require
--allow-unsafe.
Global Options
--json: machine-readable single-object output--verbose: verbose logging--no-hooks: disable hooks for this run (requires--allow-unsafe)--allow-unsafe: explicit unsafe override--no-gh: disable GitHub CLI based PR status checks for this run--hook-timeout-ms <ms>: hook timeout override--lock-timeout-ms <ms>: repository lock timeout override
Command Guide
init
vw initWhat it does:
- Creates
<worktreeRoot>/and.vde/worktree/* - Appends managed entries to
.git/info/exclude - Creates default hook templates
list
vw list
vw list --json
vw list --no-gh
vw list --full-pathWhat it does:
- Lists all worktrees from Git porcelain output
- Includes metadata such as branch, path, dirty, lock, merged, PR status, and upstream status
- JSON metadata includes
pr.statusandpr.urlfor each non-base branch - In table output, long
pathvalues are truncated with…to fit terminal width by default - Use
--full-pathto disable path truncation in table output - With
--no-gh, skips PR status checks (pr.statusbecomesunknown,merged.byPRbecomesnull) - In interactive terminal, uses Catppuccin-style ANSI colors
status
vw status
vw status feature/foo
vw status --jsonWhat it does:
- Shows one worktree state
- Without branch argument, resolves current worktree from current
cwd
path
vw path feature/foo
vw path feature/foo --jsonWhat it does:
- Resolves and returns the absolute worktree path for the target branch
new
vw new
vw new feature/fooWhat it does:
- Creates a new branch + worktree under configured managed worktree root (
paths.worktreeRoot) - Without argument, generates
wip-xxxxxx
switch
vw switch feature/fooWhat it does:
- Idempotent branch entrypoint
- Reuses existing worktree if present, otherwise creates one
mv
vw mv feature/new-nameWhat it does:
- Renames current non-primary worktree branch and moves its directory
- Requires branch checkout (not detached HEAD)
del
vw del
vw del feature/foo
vw del feature/foo --force-unmerged --allow-unpushed --allow-unsafeWhat it does:
- Removes worktree and branch safely
- By default, rejects dirty, locked, unmerged/unknown, or unpushed/unknown states
Useful force flags:
--force-dirty--allow-unpushed--force-unmerged--force-locked--force(enables all force flags)
gone
vw gone
vw gone --apply
vw gone --jsonWhat it does:
- Bulk cleanup candidate finder/remover
- Default mode is dry-run
--applyactually deletes eligible branches/worktrees
adopt
vw adopt
vw adopt --json
vw adopt --applyWhat it does:
- Finds unmanaged non-primary worktrees and plans moves into the managed worktree root
- Default mode is dry-run;
--applyrunsgit worktree move - Reports skipped entries with reasons (
detached,locked,target_exists,target_conflict)
get
vw get origin/feature/fooWhat it does:
- Fetches remote branch
- Creates tracking local branch when missing
- Creates/reuses local worktree
extract
vw extract --current
vw extract --current --stashWhat it does:
- Extracts current primary worktree branch into managed worktree root (
paths.worktreeRoot) - Switches primary worktree back to base branch
--stashallows extraction when primary is dirty
Current limitation:
- Implementation currently supports primary worktree extraction flow.
absorb
vw absorb feature/foo --allow-agent --allow-unsafe
vw absorb feature/foo --from feature/foo --keep-stash --allow-agent --allow-unsafeWhat it does:
- Moves changes from non-primary worktree to primary worktree, including uncommitted files
- Stashes source worktree changes, checks out branch in primary, then applies stash
--fromaccepts vw-managed worktree name only (<worktreeRoot>/...path prefix is rejected)
Safety:
- Rejects dirty primary worktree
- In non-TTY mode, requires
--allow-agentand--allow-unsafe --keep-stashkeeps the stash entry after apply for rollback/debugging
unabsorb
vw unabsorb feature/foo --allow-agent --allow-unsafe
vw unabsorb feature/foo --to feature/foo --keep-stash --allow-agent --allow-unsafeWhat it does:
- Pushes changes from primary worktree to non-primary worktree, including uncommitted files
- Stashes primary worktree changes, applies stash in target worktree
--toaccepts vw-managed worktree name only (<worktreeRoot>/...path prefix is rejected)
Safety:
- Requires primary worktree to be on target branch
- Rejects clean primary worktree
- Rejects dirty target worktree
- In non-TTY mode, requires
--allow-agentand--allow-unsafe --keep-stashkeeps the stash entry after apply for rollback/debugging
use
vw use feature/foo
vw use feature/foo --allow-shared
vw use feature/foo --allow-agent --allow-unsafeWhat it does:
- Checks out the target branch in the primary worktree
- Intended for human workflows where primary context must be fixed
Safety:
- Rejects dirty primary worktree
- If target branch is attached by another worktree, requires
--allow-sharedand prints a warning - In non-TTY mode, requires
--allow-agentand--allow-unsafe
exec
vw exec feature/foo -- pnpm test
vw exec feature/foo --json -- pnpm testWhat it does:
- Executes command inside the target branch worktree path
- Does not use shell expansion
Exit behavior:
- Child success =>
0 - Child failure =>
21(CHILD_PROCESS_FAILEDin JSON mode)
invoke
vw invoke post-switch
vw invoke pre-new -- --arg1 --arg2What it does:
- Manually invokes
pre-*/post-*hook scripts - Useful for debugging hook behavior
copy
vw copy .envrc .claude/settings.local.jsonWhat it does:
- Copies repo-relative files/dirs from repo root into target worktree
- Primarily intended for hook usage with
WT_WORKTREE_PATH
link
vw link .envrc
vw link .envrc --no-fallbackWhat it does:
- Creates symlink in target worktree pointing to repo-root file
- On Windows, can fallback to copy unless
--no-fallback
lock / unlock
vw lock feature/foo --owner codex --reason "agent in progress"
vw unlock feature/foo --owner codex
vw unlock feature/foo --forceWhat they do:
lockwrites lock metadata under.vde/worktree/locks/unlockclears lock, enforcing owner match unless--force
cd
cd "$(vw cd)"What it does:
- Interactive worktree picker via
fzf - Picker list shows worktree branch names with minimal states (dirty/merged/lock)
- Preview pane shows path and worktree states (dirty/locked/merged/upstream)
- Picker and preview use Catppuccin-style ANSI colors in interactive terminal
- Prints selected absolute path to stdout
completion
vw completion zsh
vw completion fish
vw completion zsh --installWhat it does:
- Prints completion script for zsh/fish
- With
--install, writes completion file to shell default path or--path
Merge Status (Local + PR)
Each worktree reports:
merged.byAncestry: local ancestry check (git merge-base --is-ancestor <branch> <baseBranch>)merged.byPR: PR-based merged check via GitHub CLImerged.overall: final decisionpr.status: PR state (none/open/merged/closed_unmerged/unknown)pr.url: latest PR URL for the branch (nullwhen unavailable)
Overall policy:
byPR === true=>overall = true(includes squash/rebase merges)byAncestry === false=>overall = false- when
byAncestry === true, require divergence evidence before treating as merged- lifecycle evidence from
.vde/worktree/state/branches/*.json - reflog fallback (
git reflog) when lifecycle evidence is missing
- lifecycle evidence from
- if divergence evidence is contained in
baseBranch,overall = true byPR === falseor explicit lifecycle "not merged" evidence =>overall = false- otherwise
overall = null
byPR becomes null and pr.status becomes unknown when PR lookup is unavailable (for example: gh missing, auth missing, API error, github.enabled=false in config.yml, or --no-gh).
JSON Contract
With --json, stdout always emits exactly one JSON object.
Common success fields:
schemaVersioncommandstatusrepoRoot
Error shape:
status: "error"codemessagedetails
Configuration (config.yml)
Configuration is loaded from:
$XDG_CONFIG_HOME/vde/worktree/config.yml(fallback:~/.config/vde/worktree/config.yml).vde/worktree/config.ymldiscovered fromcwdto the local Git boundary (.git)<repoRoot>/.vde/worktree/config.yml(always considered, including linked worktree execution)
Supported keys (examples):
paths:
worktreeRoot: .worktree
git:
baseBranch: null
baseRemote: origin
github:
enabled: true
hooks:
enabled: true
timeoutMs: 30000
locks:
timeoutMs: 15000
staleLockTTLSeconds: 1800
list:
table:
columns: [branch, dirty, merged, pr, locked, ahead, behind, path]
selector:
cd:
prompt: "worktree> "
surface: auto # auto | inline | tmux-popup
tmuxPopupOpts: "80%,70%"Notes:
paths.worktreeRootaccepts repo-relative and absolute paths- Paths under
.gitare allowed (for example:.git/worktrees) - If
paths.worktreeRootpoints to an existing file, config loading fails
Current Scope
- Ink-based
tuiis not implemented yet.
