A Herdr plugin that manages Git branch worktrees from one fast, modal fzf popup — list, open, create, and safely remove worktrees across every repository Herdr knows about, without leaving your keyboard.
- Plugin ID:
ludoroo.forestr - Action:
ludoroo.forestr.open - Version:
0.2.1 - Platforms: Linux, macOS
Every worktree, one place — regardless of repository size. Forestr gives you one consistent interface for the worktrees behind all of your Herdr workspaces, from small repositories to large monorepos. Fast operations stay immediate, while potentially expensive enrichment and approved removals run asynchronously so large repositories do not make the interface feel stuck. Forestr drives your backend of choice — native Git or Worktrunk — without hiding or bypassing its checks and hooks.
Forestr discovers repositories through Herdr workspace metadata, the pane working directory of plain workspaces, and the repository you launched it from, then lists their worktrees in a single table: repository, branch, state, HEAD, and path. The row for your current checkout renders instantly; the rest stream in without blocking.
Enter opens the selected worktree and focuses (or creates) its Herdr workspace. c starts a repository-first create wizard: pick a repository, pick a local or remote branch — or type a new one — and Forestr materialises the checkout and jumps to it.
d / D approve a removal and hand it to a detached worker that survives closing the popup, pressing q/Esc, Ctrl+C, and workspace switches. Progress shows in the footer while it runs. The result lands in the footer if the popup is still open, or arrives as a Herdr notification if it isn't.
If a removal is interrupted after Git has already mutated (for example a large deletion killed mid-way), the next launch reconciles it against git worktree list and closes only the exactly matching stale workspace — never a worktree that is still registered, still on disk, or unverifiable.
Removing the worktree you launched Forestr from focuses its parent workspace before any mutation starts, so you are never left inside a checkout that is being deleted.
p toggles a bounded, fixed-width commit history for the selected worktree (its own HEAD ancestry only, capped at 25 commits) with compacted change statistics. It moves below the list when the side panel is too narrow.
The popup draws with your terminal's default colours and ANSI palette, so it matches whatever Herdr theme you run — no colour configuration needed.
backend = "auto" uses Worktrunk (wt) when available and native Git otherwise.
| Backend | Highlights |
|---|---|
| Git | Pure porcelain. Exact local/remote branch materialisation, sibling checkout paths (.<repository>-<branch>), conservative deletion with unmerged-branch retention. |
| Worktrunk | Delegates switch/create/remove to wt, including clobber-create and relocation-aware operations. Background enrichment adds Worktrunk's seven-slot status symbols; each icon is overridable via status_icon_*. Hooks stay enabled. |
herdr plugin install ludoroo/forestrThen open Forestr from Herdr's action picker with ludoroo.forestr.open.
Herdr does not install plugin dependencies; make sure these are on your PATH (Linux or macOS):
| Dependency | Notes |
|---|---|
Herdr >= 0.9.0 |
|
Bash >= 3.2 |
macOS's native /bin/bash is supported |
Git, jq, curl |
|
fzf >= 0.74 |
|
Worktrunk (wt) |
optional — enables the Worktrunk backend |
Removal workers are detached with setsid (part of util-linux on Linux) or the system Perl on macOS; both are present by default.
On macOS:
brew install jq fzfForestr prefers a supported Bash from PATH, with the system and common Homebrew paths as fallbacks. macOS's native /bin/bash works without any additional installation, while a newer Bash generally provides faster startup and worker execution when already available. Explicit BASH_BIN, HERDR_BIN, FZF_BIN, GIT_BIN, JQ_BIN, CURL_BIN, and WORKTRUNK_BIN overrides are honoured. An explicit Bash override must be Bash 3.2 or newer; an invalid override fails rather than silently selecting another interpreter.
herdr plugin uninstall ludoroo.forestrRemoval records under ${XDG_STATE_HOME:-$HOME/.local/state}/forestr/ are left in place as an audit trail; delete them manually if you no longer want them.
| Key | Action |
|---|---|
j / k, g / G |
Move down/up, first/last |
Enter |
Open or choose the selected item |
c |
Start the create wizard |
d / D |
Remove / force-remove the selected worktree (approval is final) |
p |
Toggle the commit-log preview |
/ |
Search |
Ctrl-R |
Refresh |
h / Esc |
Back / close |
q |
Quit |
Create wizard, source screen: l / r / b switch between local, remote, and both branch scopes; n types an exact new branch name; C requests clobber-create (Worktrunk only).
Every key is remappable in config.toml; defaults are in config.example.toml.
herdr plugin config-dir ludoroo.forestrCopy config.example.toml to config.toml in that directory and change only what you need. Configuration is parsed as data — never sourced as shell.
| Setting | Default | Purpose |
|---|---|---|
popup_width / popup_height |
90% / 85% |
Popup size in cells or percent |
backend |
auto |
auto, git, or worktrunk |
create_scope |
local |
Branch scope offered first in the wizard |
create_base |
"" |
Base ref for new branches (Git backend) |
enrich_backend |
true |
Worktrunk status enrichment |
worktrunk_enrichment_collection_timeout_ms |
5000 |
Passed to Worktrunk's list.timeout-ms |
worktrunk_enrichment_concurrency |
2 |
1 or 2 parallel enrichment jobs |
status_icon_* |
see example | Per-slot Worktrunk status glyph overrides |
key_* |
see example | Key bindings |
- Approve —
d/Dverifies the row is a registered worktree, snapshots the matching Herdr workspace, and takes an atomic per-worktree lock so the same checkout cannot be queued twice. - Detach — A worker starts in its own OS session from the repository's primary checkout. Herdr shutting down the popup terminal cannot reach it.
- Mutate — The backend runs with hooks and safety checks intact (
wt remove --foreground --format=jsonor native Git). Its JSON result, including branch outcomes such asretained_unmerged, is parsed rather than inferred from the exit status. - Prove — Regardless of what the backend reported, Forestr probes Git from the primary checkout. Only unregistered + path gone counts as removed; registered, present, or unknown retains the worktree.
- Reconcile — The Herdr workspace is closed only if its ID and checkout path still match the snapshot, after a second probe. Results are persisted and notified.
- Recover — On every launch, queued/running records whose exact worker (PID, start time, and argv) is gone are reconciled with the same rules. Recovery only observes; it never re-runs a destructive command.
There is intentionally no post-approval cancel key: neither backend exposes a reliable boundary between "safe to abort" and "already irreversible".
The native Git backend:
- never removes the primary worktree, runs
git worktree prune, deletes arbitrary directories, or rewrites unrelated refs; - refuses path collisions, ambiguous or missing refs, cross-repository paths, and selections that changed since the list was rendered;
- creates exact tracking branches for remote selections and does not infer tracking for typed names;
- uses normal deletion for
d(retaining unmerged or checked-out branches with a warning) and force deletion only for explicitD; - leaves a freshly created checkout in place if post-create verification fails, so it can be inspected rather than destructively rolled back.
Worktrunk operations keep Worktrunk's own hooks and checks; Forestr never passes --no-hooks or --yes.
- Removal cannot be cancelled once approved. Close the popup freely; the job still completes and reports. This is a deliberate safety trade-off (see above).
- Closing an active workspace defers to Herdr. If you navigate back into a worktree that is being removed, Herdr — not Forestr — chooses the next focused workspace when it closes.
- Removal records are not pruned automatically. They are small JSON/log files kept as an audit trail under
~/.local/state/forestr/removals.

