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
Platform: macOS only for now. Tested on macOS; Linux support is planned.
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.
curl -fsSL https://bun.com/install | bash
brew install fzfOptional: bat for richer README.md previews (brew install bat).
herdr plugin install andrewchng/herdr-sessionizer --yes
herdr plugin config-dir sessionizerWire keybindings in your Herdr config (see Example keybindings).
bun install
herdr plugin link /path/to/herdr-sessionizerAfter manifest or pane/action changes:
herdr plugin unlink sessionizer || true
herdr plugin link /path/to/herdr-sessionizer| 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
Worktree (always starts at repo picker)
projects ──> branches? ──Enter──> reopen or create — see table
└──────────── Esc / none ──> type new branch → create + layout
| Selection | Result |
|---|---|
| Existing workspace/checkout | Reopen as-is |
| 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 |
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, it applies the layout from config.toml. Existing workspaces are only focused — the layout is not reapplied.
~/.config/herdr/plugins/config/sessionizer/config.toml
Created automatically on first run if missing.
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
[layout]
placement = "overlay"
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[layout].placement— how plugin panes open (overlayorsplit)[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 [layout].placement 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 |
| Focus or reopen 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 - sessionizer-gh-release — ship a version (changelog, tag, GitHub release)
npx skills add andrewchng/herdr-sessionizer --list
npx skills add andrewchng/herdr-sessionizer --skill sessionizer-layout-editor -y -g
npx skills add andrewchng/herdr-sessionizer --skill sessionizer-gh-release -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 release -- <version> --dry-run
bun run release:tag -- <version> --dry-run
bun run release:notes -- <version>
bun run sessionizerUse 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.
