Documentation menu

Projects

Projects are declarative herdr workspace templates — pick one from a full-screen fuzzy browser and herdr-plus spins up a whole workspace of tabs and panes.

Projects are declarative herdr workspace templates that spin up a fully laid-out workspace with a single keypress.

What it does

Trigger the cloudmanic.herdr-plus.projects action — from herdr’s action menu, or a bound key — and herdr-plus opens a full-screen fuzzy browser of your projects.

Fuzzy-find a project, press enter (or click it), and herdr-plus spins up a whole workspace — every tab created, every split laid out, and every startup command running — then drops you straight into it. Cancel with esc to close the browser and return to where you were.

Note: With no project files yet, the browser shows an onboarding screen explaining how to add your first project.

What a project is

A project is a declarative herdr workspace template: a name, a description, a working directory, and an ordered list of tabs (each with an optional startup command, or a set of split panes). It replaces hand-written workspace shell scripts with a simple config file.

Where projects live

Projects live in the projects/ subdirectory of herdr-plus’s config directory, one TOML file per project. The file name doesn’t matter — only its contents. Find the directory with:

herdr plugin config-dir cloudmanic.herdr-plus

The directory starts empty: unlike quick-actions, it is never seeded with examples, because an empty directory is meaningful (it triggers the onboarding screen). To add a project, drop a .toml file in. To remove a project, delete its file.

The project schema

Here is a complete, annotated example covering every field:

# Top-level project fields.
name = "Options Cafe"                                      # required: the workspace label
description = "The main options.cafe monorepo"             # shown in the browser
group = "Cloudmanic"                                       # optional: cluster a client's projects under a heading
working_dir = "~/Development/options-cafe/options.cafe"    # ~ and $VARS expand

# Tabs open in file order. The FIRST tab reuses the workspace's root tab; the
# rest are created behind it so the first tab stays in front.

[[tabs]]
name = "claude"                                            # required: the tab label
command = "claude --dangerously-skip-permissions --chrome" # runs on startup

[[tabs]]
name = "lazygit"
command = "lazygit"

[[tabs]]
name = "terminal"                                          # no command — just an empty shell

name

Required. The workspace label herdr-plus gives the project’s workspace, and the text you fuzzy-find in the browser.

description

Optional free text shown next to the name in the browser.

group

Optional. A label that clusters related projects in the browser — see Grouping projects by client. It has no effect on the workspace that opens; it only changes how the browser is laid out.

working_dir

The directory the workspace is rooted at. A leading ~ expands to your home directory, and any $VARS in the path are expanded from your environment. An empty (or ~) working_dir defaults to your home directory, so a minimal project still opens somewhere sensible.

Important: The working directory’s existence is checked when you open the project, not when the file is loaded. (The same project file might be valid on one machine and not another.) If the directory doesn’t exist, opening the project fails with a clear error.

[[tabs]]

A project needs at least one [[tabs]] entry. Tabs are created in file order. Each tab has:

  • namerequired, the tab’s label.
  • command — optional. The startup command, run as if typed at the prompt. A tab with no command (and no panes) is just an empty shell.
  • working_dir — optional. The directory this tab starts in, overriding the project’s. See A directory per tab.

The first tab reuses the workspace’s root tab (it’s renamed to the tab’s name); every later tab is created behind it.

A directory per tab

The project’s working_dir is where every tab starts. A tab can override it, which is what a monorepo usually wants — the frontend and the backend each in their own subdirectory:

name = "Shop"
working_dir = "~/dev/shop"

[[tabs]]
name = "web"
working_dir = "frontend"   # relative to the project → ~/dev/shop/frontend
command = "npm run dev"

[[tabs]]
name = "api"
working_dir = "backend"    # → ~/dev/shop/backend
command = "make run"

[[tabs]]
name = "shell"             # no working_dir → the project's ~/dev/shop

A relative path is resolved against the project’s working_dir, so "frontend" means what it looks like rather than something relative to wherever herdr happens to be running. Absolute paths, ~, and $VARS behave exactly as they do at the project level.

Every directory must already exist. A missing one is reported before anything opens, so a typo never leaves a half-built workspace behind.

Split panes within a tab

A tab can hold up to 4 panes. Instead of a single command, give the tab [[tabs.panes]] entries:

[[tabs]]
name = "server"

[[tabs.panes]]
command = "php artisan serve"

[[tabs.panes]]
command = "npm run dev"
split = "down"

Each pane:

  • command — optional. The command to run in that pane on startup. A pane with no command is an empty shell.
  • split — how the pane is created relative to the previous pane in the tab:
    • "down" — stacked below (top/bottom).
    • "right" — beside it (side by side).
    • Omitted — defaults to "down".
  • ratio — optional. How much of the split this pane takes, between 0 and 1. Omitted, the split is even.
  • working_dir — optional. The directory this pane starts in. Omitted, it inherits its tab’s. See A pane in its own directory.

The first pane is the tab’s root, so its split and ratio are ignored. Each later pane splits off the one before it.

Sizing a split

ratio is the share the pane it is written on gets; the pane it splits off keeps the rest. Here the editor fills three quarters of the tab and lazygit sits in the last quarter:

[[tabs]]
name = "editor"

[[tabs.panes]]
command = "nvim"

[[tabs.panes]]
command = "lazygit"
split = "right"
ratio = 0.25

A ratio outside 01 (a percentage like 30, say) is a load-time error rather than a silently clamped layout.

A pane in its own directory

Panes start in their tab’s directory. A pane that wants a different one says so, and the rest of the tab is unaffected:

[[tabs]]
name = "stack"
working_dir = "frontend"

[[tabs.panes]]
command = "npm run dev"          # frontend/

[[tabs.panes]]
command = "psql shop"
split = "right"
working_dir = "../backend/db"    # its own directory instead

A pane’s working_dir resolves the same way a tab’s does — relative to the project’s working_dir, with ~ and $VARS expanded.

command vs. [[tabs.panes]] are mutually exclusive

A tab uses either command or [[tabs.panes]], not both. Setting both is a config error reported when the project loads. A tab with neither is a single empty shell.

Tip: In the projects browser, split tabs are shown with a ×N pane count (e.g. server ×2) so you can see the layout at a glance.

A tab may declare at most 4 panes; more than that is a load-time error.

Grouping projects by client

Give a project a group and the browser clusters projects that share that label under a heading. This is built for the common case where one client has several projects — tag each with the client’s name and they sit together:

name = "Acme — Web"
group = "Acme Co."
working_dir = "~/Clients/acme/web"

[[tabs]]
name = "editor"
command = "spiceedit"

How the browser lays out:

  • Headings appear only when used. If no project sets a group, the browser is a plain, flat list — nothing changes.
  • Named groups come first, in case-insensitive alphabetical order by group name. A client’s projects keep their usual name order under the heading.
  • Group-less projects fall under a catch-all Ungrouped heading at the bottom, so a mix of grouped and ungrouped projects is always clearly labeled.
  • Search is unchanged. Start typing and the headings drop away — you filter a single ranked list across every project, grouped or not.

Opening a project by name

The browser is interactive, but a project can also be opened directly — no picker, no fuzzy-typing — with the open subcommand:

herdr-plus open harbor-sysadmin

It loads the same project templates the browser shows, resolves the one whose name matches — exactly first, then case-insensitively, so open Harbor-Sysadmin still finds harbor-sysadmin — and builds its workspace through the same code path the browser uses. There is one source of truth for turning a template into a live workspace. A name that matches nothing fails with the list of what is available.

Run it from inside herdr — a pane’s shell, a keybinding, a script, another tool. Like every herdr-plus command it reaches the running herdr over its socket, and it asks herdr where your templates live, so it finds exactly the ones the browser shows.

That makes it the scriptable way to spin up a known workspace in one shot, which is handy for the two or three projects you open constantly:

alias work='herdr-plus open my-work-app'

To get herdr-plus onto your PATH, see Just the binary — installing the plugin alone does not put it there.

Note: A project whose working_dir is {prompt} can’t be opened this way — there’s no picker to ask for the path. Use the browser for those.

Adding and removing projects

The model is one file per project:

  • Add a project — drop a new .toml file into the projects/ subdirectory of herdr-plus’s config dir.
  • Remove a project — delete its file.

Projects are sorted by name in the browser, and clustered under group headings when any project sets a group. A malformed or invalid project file fails the whole load with a message naming the offending file, so a config mistake surfaces loudly instead of a project silently going missing.

See also

Spot something off? Edit this page on GitHub ↗