Skip to content

About

Herdr Plugin to integrate worktrunk for git worktree management

Topics

Resources

Stars

184 stars

Watchers

1 watching

Forks

Repository files navigation

Worktrunk

A herdr plugin for switching, creating, merging, and removing git worktrees through worktrunk. Pick (or type) a branch in an fzf picker and open the worktree as a herdr tab or a native worktree workspace — with worktrunk's hooks running along the way.

Why this plugin

herdr already ships with its own worktree management (herdr worktree create/open/remove/list), and it works fine. But worktrunk is a dedicated worktree manager that does more — most importantly, lifecycle hooks: run setup when a worktree is created (install deps, copy .env files, bootstrap services) and teardown when it's removed, with template variables like {{ branch }} and {{ worktree_path }}. herdr's built-in worktree commands have no hook system.

Rather than reimplement hooks inside herdr, this plugin wires worktrunk's wt into herdr: you get worktrunk's hook-driven workflow (plus its niceties — base branch selection, PR shortcuts, live preview) while choosing whether the resulting worktree opens as a tab or as a native linked-worktree workspace.

What it does

Six workspace actions:

  • Worktree: switch / create from default branch — opens an fzf picker over your existing worktrees and local branches without worktrees (remote-tracking branches too, if enabled — see Remote branches in the picker). Press Enter on a match to switch to it, or type a new name and press Enter to create it from worktrunk's default base branch. When the name you want fuzzy-matches an existing branch (e.g. you want pgx but pgx-bump exists), press Alt+Enter to force the typed name instead of the highlighted match.

  • Worktree: switch / create from current branch — the same picker, but typed new branch names are created with wt switch --create --base @, i.e. from the currently checked-out branch/worktree.

  • Worktree: switch / create from local or remote branches — the default-base picker with remote-tracking branches included for this invocation.

The pickers support worktrunk syntax for PR/MR along with other shortcuts. Worktrunk's lifecycle hooks run in either presentation mode, and the checkout opens as a tab or a native worktree workspace according to plugin configuration.

  • Worktree: remove — opens an fzf picker over removable worktrees (everything except the main checkout). Pick one; worktrunk prompts for confirmation and gates unmerged branches / untracked files itself, then removes it. The native workspace or any legacy tab panes associated with the deleted worktree are closed automatically.

  • Worktree: merge into the target branch — the same picker over removable worktrees, then wt merge on the one you pick, then removal. The native workspace or legacy tab panes are closed once the worktree is gone.

  • Worktree: merge into the target branch, keeping every commit — the same merge with --no-squash. See Merge flags for the rest.

Worktree presentation

By default the plugin organizes worktrees the same way as herdr's built-in worktree support: each checkout becomes a nested worktree workspace in the sidebar. To restore the original tab-based behavior, set open_mode to "tab" in the plugin's managed configuration directory:

config_dir=$(herdr plugin config-dir worktrunk)
mkdir -p "$config_dir"
${EDITOR:-vi} "$config_dir/config.toml"
open_mode = "tab"

Supported values:

  • open_mode = "workspace" — let Worktrunk create or switch the checkout and run its hooks, then register that checkout with herdr worktree open. Herdr displays it as a nested worktree workspace in the sidebar. This is the default.
  • open_mode = "tab" — open a new tab in the current workspace and run wt in that tab's shell. This preserves the original plugin behavior; see Tab mode and your shell.

The config file is read each time the picker runs, so changing the mode does not require reinstalling or reloading the plugin.

Tab mode and your shell

In tab mode the plugin can't run wt itself: the new tab has to end up inside the worktree, and only the shell running in that tab can change its own directory. So the picker types a wt switch … line into the tab's interactive shell, written in that shell's own syntax, followed by a small relabel step (tab-relabel.sh) that renames the tab after the branch the switch resolved to. The plugin asks herdr which shell the tab runs and supports bash, zsh, fish, and nushell; any other shell gets the POSIX form.

This needs worktrunk's shell integration installed for that shell — run wt config shell install and restart the shell — because it is the integration's wt command that moves the shell into the worktree. Without it the worktree is still created, but the tab stays where it was; the plugin says so in the tab and leaves it labeled with the name you picked.

Workspace mode (the default) calls wt directly and needs none of this. On Windows, tab mode works differently; see Windows.

Remote branches in the picker

By default the picker lists only your worktrees and local branches. To also offer remote-tracking branches (e.g. origin/foo; run git fetch yourself to refresh these), set show_remote_branches to true in the same config.toml:

show_remote_branches = true

Local branches without worktrees always appear regardless of this setting.

Branch names from free text

A new branch is created with the name exactly as typed. To turn a pasted title like Fix Login Bug into fix-login-bug, set slugify_new_branches in the same config.toml:

slugify_new_branches = true

Existing branches and worktrunk shortcuts are never changed.

Merge flags

The merge actions pass no flags to wt merge beyond the variant's --no-squash. To change what every merge does, list flags in merge_flags in the same config.toml:

merge_flags = "--no-squash --no-rebase"

Accepted: --no-squash, --no-rebase, --no-ff, --no-commit, --no-hooks, --stage=all|tracked|none (see wt merge --help).

A merge that fails leaves the worktree and its workspace alone, with worktrunk's output on screen.

Holding the pane on success

When worktrunk succeeds the picker pane closes straight away, taking the output of wt and its hooks with it — most noticeably in a popup. To keep the pane up until you press a key, set hold_on_success in the same config.toml:

hold_on_success = true

To hold only some actions, set their own keys instead. An action's key wins over hold_on_success, so it can also switch a single action back off:

hold_on_success = true
hold_on_create = false
  • hold_on_create — after wt switch created a worktree, before its workspace opens. Switching to an existing worktree never holds, and neither does tab mode, where wt runs in the tab you keep.
  • hold_on_merge — after wt merge and the removal that follows it.
  • hold_on_remove — after wt remove.

All four default to false. A failure always keeps the pane up, whatever these are set to.

Picker presentation

The picker opens in a split pane below the workspace. To open it as a session-modal popup over the current layout instead, set picker_placement in the same config.toml:

picker_placement = "popup"

Supported values:

  • picker_placement = "split" — a pane split below the workspace, closed when the picker exits. This is the default.
  • picker_placement = "popup" — a floating terminal centered over the tab, leaving the tiled layout alone. Needs herdr ≥ 0.7.4.

A popup is half the window by default. Size it with popup_width and popup_height, either as terminal cells or as a percentage of the window:

picker_placement = "popup"
popup_width = "70%"
popup_height = 24

In a split the picker draws its own rounded border and inset margin, which a popup does not need, so the list fills the popup frame herdr already draws.

Requirements

  • herdr ≥ 0.7.0 (Windows support was tested with 0.9.2-preview)
  • worktrunk ≥ 0.60.0 — the wt CLI on your PATH
  • fzf — the interactive picker
  • jq — JSON parsing (macOS/Linux only; the Windows scripts parse JSON natively)
  • bash — the scripts run with /bin/bash (macOS/Linux only)

Platforms: macOS, Linux, and Windows (herdr's Windows plugin support is in preview).

Windows

Windows support can lag behind macOS and Linux, and a new feature may be missing on Windows for a while. Features land in the bash scripts first; their PowerShell ports come from contributors who can test on Windows. If something works on macOS or Linux but not on Windows, open an issue and say it is Windows.

The actions run as Windows PowerShell 5.1 scripts (*.ps1) — nothing beyond stock Windows is needed for the scripts themselves, and jq is not used. Install the two external tools with winget:

winget install max-sixty.worktrunk junegunn.fzf

Worktrunk's binary is named wt.exe, which collides with Windows Terminal's wt.exe launcher alias — and the alias usually wins on PATH. The plugin resolves worktrunk itself: it tries the WORKTRUNK_BIN environment variable, then worktrunk_bin in the plugin's config.toml, then a worktrunk/wt on PATH that is not the Windows Terminal alias, then ~\.cargo\bin\wt.exe. If worktrunk lives somewhere unusual, point at it explicitly:

worktrunk_bin = 'C:\path\to\wt.exe'

Two Windows behaviors differ by design:

  • Removal order. Windows refuses to delete a directory that is any process's cwd — and the worktree's own workspace pane is exactly such a process. The remove and merge actions therefore close the worktree's herdr UI before wt remove runs (the reverse of the Unix scripts), and reopen the workspace if the removal then fails or is declined. Panes closed in tab mode cannot be brought back that way — decline a removal there and its shells stay closed. When the action runs from inside the worktree's own workspace, that workspace is closed only after the removal succeeds. So hold_on_remove and hold_on_merge wait for the key after the worktree's workspace has already closed, except when the action runs inside it.
  • Tab mode (open_mode = "tab") sends a PowerShell command into the new tab, so it expects a PowerShell-family default shell. It does not rely on worktrunk's shell integration: the sent command switches with --no-cd and changes into the worktree directory itself.

Installation

From the herdr CLI:

herdr plugin install devashish2203/herdr-worktrunk

Or, for local development, clone and link:

git clone https://github.com/devashish2203/herdr-worktrunk
herdr plugin link /path/to/herdr-worktrunk

Usage

On Windows, append -windows to every action id below (open-windows, open-current-windows, open-with-remotes-windows, remove-windows, merge-windows, merge-no-squash-windows) — they are the same actions backed by the PowerShell scripts.

Create/Switch a worktree from the default branch

herdr plugin action invoke open --plugin worktrunk

Create/Switch a worktree from the current branch

herdr plugin action invoke open-current --plugin worktrunk

Create/Switch a worktree from local or remote branches

herdr plugin action invoke open-with-remotes --plugin worktrunk

Remove Worktree

herdr plugin action invoke remove --plugin worktrunk

Keybindings

To drive the plugin from the keyboard, add [[keys.command]] entries to ~/.config/herdr/config.toml (%APPDATA%\herdr\config.toml on Windows) with type = "plugin_action". The command is the plugin's action id qualified with the plugin id (worktrunk.<action>; run herdr plugin action list to see the ids — on Windows use the -windows ids, e.g. worktrunk.open-windows):

# Override herdr's built-in "new worktree" key (prefix+shift+g) with worktrunk's
# default-branch switch/create picker:
[[keys.command]]
key = "prefix+shift+g"
type = "plugin_action"
command = "worktrunk.open"
description = "Worktree: switch / create from default branch"

# Optional: bind current-branch creation separately.
[[keys.command]]
key = "prefix+shift+c"
type = "plugin_action"
command = "worktrunk.open-current"
description = "Worktree: switch / create from current branch"

# Optional: include remote-tracking branches for this picker.
[[keys.command]]
key = "prefix+shift+r"
type = "plugin_action"
command = "worktrunk.open-with-remotes"
description = "Worktree: switch / create from local or remote branches"

[[keys.command]]
key = "prefix+shift+d"
type = "plugin_action"
command = "worktrunk.remove"
description = "Worktree: remove"

[[keys.command]]
key = "prefix+shift+m"
type = "plugin_action"
command = "worktrunk.merge"
description = "Worktree: merge into the target branch"

Recommended: override herdr's built-in worktree management with these. herdr binds prefix+shift+g to "new worktree" by default, and a custom keybinding takes precedence over the built-in on the same key — so mapping worktrunk.open to prefix+shift+g replaces it with worktrunk's switch/create picker, hooks included. Pick matching keys for worktrunk.open-current, worktrunk.open-with-remotes, worktrunk.remove, worktrunk.merge, and worktrunk.merge-no-squash to round out the workflow.

Reload the config after editing it:

herdr server reload-config

Development

The plugin is a manifest plus small bash scripts (macOS/Linux) and their Windows PowerShell 5.1 ports (*.ps1, Windows). The manifest declares every action and pane twice — once per platform set, with -windows-suffixed ids for the PowerShell twins — using herdr's item-level platforms overrides.

  • herdr-plugin.toml — actions and panes
  • config.sh / config.ps1 — worktree and picker presentation configuration
  • helpers.sh / helpers.ps1 — shared helpers (worktrunk shortcut detection; on Windows also path normalization and worktrunk binary resolution)
  • open.sh / open.ps1 — the action entrypoint that opens a picker in its configured placement
  • picker.sh / picker.ps1 — the switch / create picker
  • tab-relabel.sh — the relabel step tab mode types into the new tab after wt switch (macOS/Linux; picker.ps1 relabels inline)
  • remove.sh / remove.ps1 — the remove picker
  • merge.sh / merge.ps1 — the merge picker
  • lifecycle.sh / lifecycle.ps1 — shared steps for the actions that destroy a worktree: candidate listing, herdr workspace resolution, UI cleanup
  • tests/config_test.sh — configuration parser checks
  • tests/helpers_test.sh — helper function checks
  • tests/lifecycle_test.sh — candidate/workspace resolution and cleanup checks
  • tests/merge_test.sh — merge argument and failure-path checks
  • tests/open_test.sh — picker placement / open argument checks
  • tests/picker_test.sh — switch / create picker checks in both open modes
  • tests/remove_test.sh — remove argument, failure-path and hold checks
  • tests/tab_relabel_test.sh — tab relabel checks
  • tests/*_test.ps1 — Windows ports of the same checks (stubbing wt, fzf, and herdr with generated .cmd shims); run them all with powershell -NoProfile -ExecutionPolicy Bypass -File tests\run_tests.ps1

herdr caches the manifest when a plugin is linked, so after editing herdr-plugin.toml you must relink for changes to take effect:

herdr plugin unlink worktrunk && herdr plugin link "$PWD"

Edits to the bash scripts are picked up on the next run — no relink needed.

License

MIT © Devashish Chandra

About

Herdr Plugin to integrate worktrunk for git worktree management

Topics

Resources

Stars

184 stars

Watchers

1 watching

Forks

Releases

Contributors

Languages