Open a new agent session in the herdr workspace that belongs to the project you are in — run from a terminal in Zed, or from anywhere else.
herdr records directories against panes, not workspaces, so this works out which workspace you mean by matching your project root against the working directory of every live pane.
If no live workspace matches, the matching herdr-spreader layout is applied with spread
and resolution is retried. Use --dry-run to see what would happen without doing it.
herdr-tab-new [name] [options]
--dry-run Show what would be created without creating it.
--workspace <id|label> Use this workspace instead of inferring one.
--kind <agent> Agent to start (default: claude).
-w, --worktree Isolate the session in its own git worktree.
--branch <name> Branch for --worktree (default: the tab name).
--list List the tabs this tool created in this session.
--new Always create, without offering existing sessions.
--attach Pick from every live agent session, across all projects.
--teardown [name] Close sessions this tool created (all, or just [name]).
Combine with --dry-run to preview.
-h, --help Show this help.
$ herdr-tab-new
Started claude in tab "compliance" (w2:t2), workspace compliance (w2)
directory /Users/andrew/hrock/src/compliance
pane w2:p2
$ herdr-tab-new --dry-run
New claude session → tab "compliance" in workspace compliance (w2)
project /Users/andrew/hrock/src/compliance (from git)
branch main
workspace compliance (w2), 1 existing tab
session default
Exit codes: 0 ok · 1 usage · 2 herdr unavailable · 3 no matching workspace ·
4 ambiguous workspace.
| Question | Answer |
|---|---|
| Which project? | ZED_WORKTREE_ROOT → git root → working directory |
| Which herdr session? | HERDR_SOCKET_PATH → HERDR_SESSION → the default session |
| Which workspace? | Any workspace with a pane at or beneath the project root. A pane above it never counts — otherwise a workspace opened at ~ would match every project on the machine. |
| Which of several? | The focused one. If none is focused it refuses and lists them, rather than guessing. |
| Does it steal focus? | No. Nothing it creates takes focus inside herdr — you are brought to the session by attaching instead. |
| What name? | The branch, unless that is main/master, in which case the project directory name. -2, -3… appended if taken. |
| No workspace at all? | Whichever layout in ~/.herdr-spreader/ declares this project root, applied via spread <name>. If there is no layout, it stops and says so. |
| Which agent? | claude, or --kind <agent>. The valid list is read from herdr agent start --help, and an unknown kind is rejected before anything is created. |
Run from inside a herdr pane, herdr already exports HERDR_SOCKET_PATH, so the right session
is targeted with no inference at all.
-w gives the session its own git worktree and branch, so an agent working in it cannot
disturb your open checkout — even with uncommitted changes in it.
$ herdr-tab-new -w --branch feat-auth
Started claude on branch "feat-auth"
worktree /Users/andrew/.herdr/worktrees/compliance/feat-auth
workspace feat-auth (wA)
tab wA:t1
pane wA:p1
Your original checkout is untouched.
This uses herdr's own worktree create, so the isolated session is a workspace rather than
a tab beside your others. That is a deliberate trade: it puts the worktree where herdr expects
(~/.herdr/worktrees/<repo>/<branch>), registers it in herdr worktree list, and means
teardown is a single herdr worktree remove --workspace <id>.
- The branch comes from
--branch, else the tab name. It branches from current HEAD. - An existing branch is reused rather than refused.
- If that worktree is already open, it is focused rather than given a second agent.
- Not a git repo? It refuses before creating anything.
--dry-runpreviews what would be made (branch new or reused, workspace label) and creates nothing. It does not print the worktree path, because herdr picks it and cannot be asked without creating the worktree.- Removing a worktree does not delete its branch.
- Zed sees these worktrees. They appear in Zed's git-panel worktree picker even though they
live outside Zed's own
git.worktree_directory— Zed enumeratesgit worktree listrather than scanning a directory. Verified against Zed directly.
Closing the terminal you attached from leaves the tab and its agent running in the herdr server — so running the command again in the same project would otherwise make a second session, and the first would quietly become invisible.
Instead, if anything is already running for this project, you are shown it and choose:
$ herdr-tab-new
1 session already open here:
1) idle · resume-probe · 2h ago
n) start a new session here
Choose:
- "Already open here" means any live agent whose directory is this project root — including sessions started by hand in herdr, or whose record teardown pruned. herdr's live view is authoritative, and attaching is non-destructive.
--newskips the picker entirely.--attachlists every live session grouped by project, which is the view that makes accumulation across all your work visible at once.--dry-runnever prompts.
Two things herdr does not expose, and their consequences:
- Whether a client is attached to a pane. So the picker shows
agent_statusinstead —workingmeans an agent is mid-task right now. - When an agent started. Ages come from this tool's own records, so a session it did not create shows no age rather than a guessed one.
Exit code 5 means a choice was needed but stdin was not a terminal — nothing was created.
Run it from a terminal outside herdr — a Zed terminal — and that terminal becomes the new
agent session, via herdr agent attach. You do not switch apps and you do not go hunting for
the tab.
Run it from inside a herdr pane and it just reports what it created: you are already in
herdr, so taking your terminal over would be rude. Detected with HERDR_ENV, which herdr sets
in every pane it spawns.
Nothing is attached for --dry-run, --list, --teardown, or when the agent failed to start —
there would be nothing to attach to.
Two things that were only learned by trying them:
-
--takeoveris required, not optional. Without it the attach connects but does not own input: nothing you type reaches the agent, and the prefix key is not processed either. -
Zed swallows herdr's
ctrl-bprefix, binding it toworkspace::ToggleLeftDock, soctrl-b qnever reaches the attach and you cannot detach. Fix it in~/.config/zed/keymap.json:[ { "context": "Terminal", "bindings": { "ctrl-b": ["terminal::SendKeystroke", "ctrl-b"] } } ]Closing the Zed terminal tab also works: the agent lives in the herdr server, not the client, so it survives.
While a small terminal is attached, herdr renders that pane at the attached terminal's size. Switch to herdr mid-attach and the pane appears clipped into a corner of the window. That is inherent to viewing one agent terminal through two differently-sized clients — detach and herdr renders it full size again.
The zed CLI can only open paths, so replacing this process is the only way to get the session
into Zed. That is why the terminal is consumed rather than a new one being opened.
Every tab it creates is recorded, so --list can show you what is yours:
$ herdr-tab-new --list
2 live, 1 gone — session "default"
live concurrent-a w2:t7 claude /Users/andrew/hrock/src/compliance
live m3-codex w2:t9 codex /Users/andrew/hrock/src/compliance (no agent)
gone m3-record-test w2:t6 claude /Users/andrew/hrock/src/compliance
Records live at <session-dir>/herdr-tab-new/tabs.jsonl — beside the herdr session they
describe, so the default and hamilton sessions never share state. They are JSONL, appended:
one short O_APPEND write is atomic, so two runs at once cannot lose each other's records,
and a truncated line from a killed process costs that line rather than the file.
Set HERDR_TAB_NEW_STATE to redirect the file (used by check:live).
$ herdr-tab-new --teardown --dry-run
This is a dry run. Teardown would:
close m5-beta (w2:tC) /Users/andrew/hrock/src/compliance
skip m5-dirty (wF:t1) the worktree has uncommitted or untracked changes
Branches are left alone — removing a worktree does not delete its branch.
Teardown works strictly from the records, so a tab it did not create cannot be touched however it is named. Beyond that:
- A worktree with uncommitted or untracked changes is never removed — it is skipped, with the reason, and its record is kept (forgetting it would strand the worktree).
- Dirtiness is judged by
git status --porcelainhere, not inferred from herdr's wording, andherdr worktree removeis then called without--forceas a second line of defence. - Removing a worktree does not delete its branch, and the summary says so.
- Records whose tab has already gone are forgotten quietly, with no herdr call.
- One session failing does not abandon the rest, and a record is only forgotten once the thing it describes is actually gone.
Pruning is the one write that is not append-only: the file is rewritten via a temp file and renamed. A record appended in that millisecond window would be lost — noted in the code, and the reason every other write is an append.
The entry point is a wrapper in dotfiles; the binary is not.
bun run build # builds dist/herdr-tab-new
ln -sfn ~/dotfiles/bin/herdr-tab-new ~/.bin/herdr-tab-new~/dotfiles/bin/herdr-tab-new is a ~1KB shell wrapper that execs the compiled binary from this
checkout, falling back to bun index.ts when it has not been built. The binary itself is ~55MB
and stays out of the dotfiles repo — committing it would more than double that repo and add
another 55MB blob on every rebuild.
Set HERDR_TAB_NEW_REPO if the checkout lives somewhere other than ~/src/herdr-tab-new.
To run it automatically when Zed opens a Terminal Thread in the agent panel, set
agent.terminal_init_command to herdr-tab-new in Zed's settings.
The same checkout is also a herdr plugin, which puts the picker on a keybinding inside herdr rather than only in a terminal.
bun run build # the manifest prefers dist/, falls back to bun
herdr plugin link . # local development
herdr plugin install softwarecrafts/herdr-tab-new # once publishedIt exposes exactly one action, softwarecrafts.tab-new.pick. There is no
separate "new session" action because the picker already offers that as a row —
and with nothing open for the project it skips the popup and just creates one.
Worktree mode, --teardown and --list stay on the CLI, which has flags and a
scrollback to print into.
Bind it in ~/.config/herdr/config.toml, then herdr server reload-config:
[[keys.command]]
key = "prefix+n"
type = "plugin_action"
command = "softwarecrafts.tab-new.pick"
description = "Resume or start an agent session"The plugin and the CLI share one record file, because herdr injects
HERDR_SOCKET_PATH and records live beside the session socket either way. A
session started from the popup is torn down by herdr-tab-new --teardown from a
terminal, and vice versa.
herdr plugin install runs bun install and bun run build, so bun has to be
on the machine — herdr reports a build failure but installs no toolchains.
herdr plugin link deliberately does not build, which is why the shim falls
back to bun index.ts.
Attach. From a terminal the tool replaces itself with herdr agent attach --takeover, so the terminal you typed in becomes the session. herdr sets
HERDR_ENV in everything it spawns, and inside herdr the equivalent move is to
focus the tab — which is what the plugin does instead. This is also why the Zed
agent.terminal_init_command route still needs the CLI: it is a terminal
outside herdr, which is precisely the case a plugin cannot serve.
bun install
bun test # 278 tests, no herdr server required — they run against captured fixtures
bun run typecheck
bun run build # compiles dist/herdr-tab-new, a standalone binary
bun run check:live # OPT-IN: creates a real tab, asserts, closes it again
bun run check:live:worktree # OPT-IN: also creates and removes a real worktreebun test never touches your live herdr. check:live deliberately does, and is kept out of
the normal suite so a routine run — including one an agent starts — can never create tabs.
All resolution logic lives in src/resolve/ as pure functions over data. Only src/system.ts
touches the outside world, which is why the tests need neither a running herdr nor a git repo.
test/fixtures/ holds real captured herdr output — including a case where two workspaces
share one directory, which is what the ambiguity handling is tested against.
- A plugin popup's stdout is not its terminal. In a
placement = "popup"pane, stdin and stderr are the pty but stdout is piped intoherdr plugin log.console.logtherefore prints where nobody can see it, and Bun'sprompt()— which writes its message to stdout — asks a question that never appears while apparently hanging. Everything the user must read goes to stderr;prompt()is used for nothing but the line read. Action commands are stricter still: they have no tty on any stream. plugin pane open --cwdchanges directory before resolving argv. A manifest command of["./bin/herdr-tab-new", …]fails withUnable to spawn <cwd>/./bin/herdr-tab-new, and manifest argv gets no shell expansion. The form that works is["sh", "-c", "exec \"$HERDR_PLUGIN_ROOT/bin/…\" …"].--placementon the CLI cannot saypopup. It accepts onlyoverlay|split|tab|zoomedon 0.8.2, so the popup placement has to be declared in the manifest and the flag omitted, or the pane is silently downgraded to a tiled one — losing the terminal input the picker depends on.- Build with
bun run build, notbun index.ts. The compiled binary has no runtime dependencies, so it behaves identically in a shell where mise, bun or node are absent. tab createreturns a pane before its shell is ready. herdr's--timeoutcovers agent readiness after launch, not the shell reaching a prompt, sostartAgentretries that one specific rejection until the shell appears.- The tab label and the agent name are not the same string. herdr allows
/, uppercase and any length in a tab label but requires^[a-z][a-z0-9_-]{0,31}$for an agent name. The label stays verbatim; the agent name is derived from it and de-duplicated against live agents. - The tab name is a starting point, not a contract. herdr plugins exist that rename tabs from the agent's own conversation; nothing should depend on the name this tool sets.