Sessionizer is a Herdr plugin that uses fuzzy pickers to open projects and Git worktrees into configured workspaces.
- Sessionizer — focus an existing workspace or create a new project workspace
- Worktree — create or reopen a Git worktree workspace, including from an open GitHub PR
Platform: macOS and Linux.
Inspired by ThePrimeagen's tmux-sessionizer: fuzzy-find a project, land in the right dev environment — but for Herdr workspaces instead of tmux sessions.
| tmux-sessionizer | Sessionizer |
|---|---|
fzf over project roots |
fzf over projects.roots |
| tmux session | Herdr workspace |
| tmux windows/panes | Sessionizer tab/pane layout |
| tmux-only | Herdr-native + worktree picker |
Sessionizer does not install system tools for you.
- Herdr
>= 0.7.4 - Bun — required for install/build (and maintainer workflows). After the plugin is built, Herdr launches compiled
dist/sessionizer— Bun is not required merely to open already-built actions/panes. - fzf — interactive pickers
curl -fsSL https://bun.com/install | bash
brew install fzfOptional: bat for richer README.md previews (brew install bat).
Optional: gh to list open GitHub PRs in the worktree picker (brew install gh && gh auth login).
herdr plugin install andrewchng/herdr-sessionizer --yes
herdr plugin config-dir sessionizerInstall runs bun install then compiles a host-local dist/sessionizer binary. Actions and panes invoke that binary with a mode (open, sessionizer, worktree-open, worktree).
Wire keybindings in your Herdr config (see Example keybindings).
herdr plugin link does not run the plugin build steps. Compile first, then link:
bun install
bun run build
herdr plugin link /path/to/herdr-sessionizerAfter TypeScript changes, bun run build is enough — Herdr execs ./dist/sessionizer on the next keypress. Relink only when herdr-plugin.toml changes:
bun run build
herdr plugin unlink sessionizer || true
herdr plugin link /path/to/herdr-sessionizerTo skip compile while iterating (keybinds run TypeScript via Bun), point the four manifest command arrays at bun run and relink. Bun must be on PATH. Restore the ./dist/sessionizer commands before committing — herdr plugin install and the published plugin always use the compiled binary.
[[actions]]
id = "open"
command = ["bun", "run", "src/sessionizer/open-pane.ts"]
[[actions]]
id = "worktree-open"
command = ["bun", "run", "src/worktree/open-worktree-pane.ts"]
[[panes]]
id = "sessionizer"
command = ["bun", "run", "src/sessionizer/sessionizer-pane.ts"]
[[panes]]
id = "worktree"
command = ["bun", "run", "src/worktree/worktree-pane.ts"]bun run sessionizer still runs the Sessionizer flow without linking or compiling.
| Flow | Action |
|---|---|
| Project picker | sessionizer.open |
| Worktree picker | sessionizer.worktree-open |
herdr plugin action invoke sessionizer.open
herdr plugin action invoke sessionizer.worktree-openSessionizer (workspace picker first; Esc → projects under projects.roots)
workspaces ──Enter──> focus
workspaces ──Esc──> projects ──Enter──> new workspace + layout + focus
└──────────── Esc / none ──> exit
Worktree (always starts at repo picker)
projects ──> branches / PRs? ──Enter──> reopen or create — see table
└──────────── Esc / none ──> type new branch → create + layout
└─ Esc ──> exit
| Selection | Result |
|---|---|
| Existing workspace/checkout | Reopen as-is |
| Open PR | Create a pr-<n> worktree (layout on first open) |
| Local branch | Create a worktree workspace for that branch |
| Remote branch | Create a local worktree from that remote branch |
| Esc / no choices | Prompt for a new branch, then create the worktree |
| Esc at branch name | Exit the worktree flow without creating anything |
Open PRs appear when gh is installed and authenticated. Drafts and fork heads are included; rows may show [draft] or [fork]. If gh is missing or fails, those rows are omitted and the rest of the picker is unchanged.
The git branch is always pr-<n>. The Herdr workspace is named pr-<n>-<short-title> so it is recognizable (e.g. pr-29-fix_worktree_gate). git pull inside the worktree tracks the live PR head, including pushes from fork contributors.
After the first open, the picker shows the existing pr-<n> workspace or checkout instead of the open-PR row. Reopening does not re-fetch or re-apply layout.
A same-repo PR can also appear as remote branch origin/<head> — both rows are shown on purpose. Fork PRs appear only as open pr.
See Layout configuration for when layout is applied.
Add these to your Herdr config, for example:
~/.config/herdr/config.toml
[[keys.command]]
key = "prefix+f"
type = "plugin_action"
command = "sessionizer.open"
description = "open project workspace"
[[keys.command]]
key = "prefix+up"
type = "plugin_action"
command = "sessionizer.worktree-open"
description = "open worktree workspace"When Sessionizer creates a new project or worktree workspace, or opens an existing worktree checkout, it applies the layout from config.toml. Focusing an existing Herdr workspace is unchanged — the layout is not reapplied.
~/.config/herdr/plugins/config/sessionizer/config.toml
Created automatically on first run if missing.
[ui], [layout], and [tabs] are optional. A config with only [projects] is
valid: new workspaces then open with a plain shell and no layout is applied.
When [tabs] sections exist, [layout].focus is required.
[ui] controls how Sessionizer / Worktree pickers open inside Herdr (not
workspace bootstrap). New configs default to overlay. You can switch to split
or popup (Herdr >= 0.7.4, session-modal at 90% width/height), or omit
[ui] entirely to fall back to overlay.
If you want an agent to help edit either the global config or a repo-local override, see Agent skills.
[projects]
roots = ["~/Projects", "~/Workspace"]
git_only = true
depth = 1
[ui]
placement = "overlay" # overlay | split | popup (popup needs Herdr >= 0.7.4)
[layout]
focus = "editor"
[tabs.dev]
label = "dev"
[[tabs.dev.panes]]
id = "editor"
title = "nvim"
command = "nvim"
[[tabs.dev.panes]]
id = "agent"
from = "editor"
title = "agent"
split = "right"
ratio = 0.3
command = "opencode"
[[tabs.dev.panes]]
id = "git"
from = "editor"
title = "lazygit"
split = "down"
command = "lazygit"
[tabs.server]
label = "server"
[[tabs.server.panes]]
id = "server"
title = "server"
command = "npm run dev"First tab shape:
dev
┌────────────────┬───────┐
│ │ agent │
│ nvim │ │
├────────────────┤ │
│ lazygit │ │
└────────────────┴───────┘
These diagrams show pane titles, not commands. Here, ratio = 0.3 gives the new right-side agent pane 30% of the split width, leaving the editor side with the remaining 70%.
Second tab shape:
server
┌──────────────┐
│ │
│ server │
│ │
└──────────────┘
[projects].roots— parent folders scanned by both pickers (plain paths; optional globs — see Glob roots below)[projects].git_only—truereturns only directories with.gitmetadata;falselists all immediate child folders[projects].depth— maximum levels below each root to scan whengit_only = true;1means immediate children[ui].placement— how Sessionizer / Worktree pickers open in Herdr (overlay,split, orpopup; new configs default tooverlay,popupneeds Herdr>= 0.7.4)[ui].width/[ui].height— popup outer size (cells or"90%"); only withplacement = "popup"[layout].focus— which tab or pane to focus after layout bootstrap[tabs.<name>]— one Herdr tab to create per section[[tabs.<name>.panes]]— panes inside the tab;from+split(rightordown) define the split treeratio— optional share for the newly created pane on the split axiscommand— exact command a pane runs (nvim,pi,claude,opencode, etc.)
Rules for ratio:
- only split-created panes may set it; the first/root pane in a tab cannot
- it must be a number greater than
0and less than1 - it is local to that split at creation time, not a percentage of the whole tab
- if omitted, Herdr's default split sizing is used
- it applies only when the workspace is first bootstrapped, never when an existing workspace is reopened
Globs in roots help when clones follow a nested layout — e.g. ghq's host/owner/repo tree — and you do not want to list every owner folder:
~/Projects/github.com/andrewchng/herdr-sessionizer
└── host ── owner ── repo
[projects]
roots = [
"~/Projects/github.com/*", # expands to each owner under github.com
"~/Projects/aur.archlinux.org", # plain path when repos sit flat under the host
]
git_only = true
depth = 1With git_only = true and depth = 1, ~/Projects/github.com/* lists repos inside each owner, not the owner folders themselves:
~/Projects/github.com/
andrewchng/
herdr-sessionizer/ ← listed
dotfiles/ ← listed
motemen/
ghq/ ← listed
The picker shows herdr-sessionizer, dotfiles, and ghq — not andrewchng or motemen. Non-git folders (e.g. not-a-repo/) are skipped.
For recursive scans (e.g. mixed or deeply nested layouts), use ** — ~/Projects/** — instead of *.
A repository can override the layout for new workspace bootstrap. Put a repo-local layout config at:
<project>/.sessionizer/config.toml
When Sessionizer or Worktree creates a new workspace at cwd, Sessionizer checks in this order:
<cwd>/.sessionizer/config.toml— if present, use its[layout].focusand[tabs.*](full replacement; no merge with global tabs)- Global
config.toml— default layout
[projects].roots and [ui] (picker placement/size) always come from the global config. Repo-local files may include those sections, but they are ignored. Invalid repo-local config fails with an error that names the file path.
| Event | Layout source |
|---|---|
| Sessionizer creates a new project workspace | Repo override at picked cwd, else global default |
| Worktree creates a new workspace | Repo override at checkout cwd, else global default |
| Worktree opens an existing checkout | Repo override at checkout cwd, else global default |
| Focus an existing workspace | No relayout |
A docs repo might skip the global nvim + agent + lazygit layout and open lazygit with an agent instead:
# my-docs-repo/.sessionizer/config.toml
[layout]
focus = "docs"
[tabs.docs]
label = "docs"
[[tabs.docs.panes]]
id = "git"
title = "lazygit"
command = "lazygit"
[[tabs.docs.panes]]
id = "agent"
from = "git"
title = "agent"
split = "right"
ratio = 0.3
command = "pi" docs
┌────────────────┬───────┐
│ │ agent │
│ lazygit │ │
│ │ │
└────────────────┴───────┘
Check .sessionizer/config.toml into the repo if you want the layout to travel with the project. Repos without it keep the global default.
This repo ships skills for agents that support the skills ecosystem:
- sessionizer-layout-editor — global Sessionizer config,
projects.roots, repo-local.sessionizer/config.tomloverrides
npx skills add andrewchng/herdr-sessionizer --list
npx skills add andrewchng/herdr-sessionizer --skill sessionizer-layout-editor -y -gExample requests:
- "Add
~/Projects/github.com/*to my Sessionizer project roots" - "Add
~/Workto my Sessionizer project roots" - "Create a repo-local override for this repo with
lazygiton the left andcopiloton the right" - "Update my global Sessionizer layout to focus the git pane"
See CHANGELOG.md for release history.
bun run typecheck
bun run test
bun run test:integration
bun run build # produces dist/sessionizer (host-local executable)
bun run release -- <version> --dry-run
bun run release:tag -- <version> --dry-run
bun run release:notes -- <version>
bun run sessionizer # dev: run Sessionizer flow via Bun without compiling
./dist/sessionizer --helpbun run test runs the unit suite only; bun run test:integration runs the real-git sandbox tests for fetchPullRequestHead (a tmpdir fake GitHub, no network). The integration suite is excluded from bun test and CI runs both — the pre-commit hook exports GIT_DIR, which would redirect the sandbox's git commands into the parent repository, so the sandbox suite only ever runs in CI's clean environment.
Create the release-prep branch from updated main (after the feature PRs merge) — never from a feature branch, or the release PR diff will include the whole feature. Use bun run release -- <version> on the release-prep branch to update version files, then run bun run release:tag -- <version> from merged main to create and push the annotated v<version> release tag.
If Herdr Sessionizer saves you a few clicks, consider sponsoring me on GitHub. Sponsorship funds ongoing maintenance — Herdr and fzf upgrades, issue triage, the small papercuts.
