Spawn the feature's pane, then hand it the task. Do this from the current session;
keep your own focus where it is (--no-focus everywhere).
a. Create the worktree FIRST — before the pane exists. You are running Bash (Git Bash),
so && is fine here; it is only the PowerShell panes that reject it. If the repo has
an origin remote, fetch and branch from origin/main; otherwise branch from main:
git fetch origin && git worktree add .claude/worktrees/<slug> -b <slug> origin/main
git worktree add .claude/worktrees/<slug> -b <slug> main
Order matters: doing this first lets the pane START inside the worktree (step c), which
buys two things. (1) herdr's recorded cwd for that pane is then honest — if you
instead Set-Location after the shell boots, herdr keeps reporting the shell's START
dir (the main checkout) forever, so every feature pane claims to be somewhere it is not.
(2) No shell chaining in the pane at all — launching Claude becomes a bare command.
b. Find which tab the pane goes in. Cap: 4 panes per tab (past ~4 the splits get
unusably small). Placement order — fill existing tabs before making new ones:
- This tab has room (< 4 panes) → put it here.
- This tab is full → use the FIRST OTHER AGENT TAB (in tab order) that has room.
Do NOT mint a new tab just because this one is full — that strands one agent per
tab and wastes the grid.
- Every agent tab is full → only then create a new tab.
Never place into one of the user's own tabs (their lazygit/shell tab). A tab is
only a candidate if EVERY pane in it is a claude agent — that is what keeps feature
panes out of e.g. the git tab.
python -c "
import json,subprocess,os
ws=os.environ['HERDR_WORKSPACE_ID']
tabs=json.loads(subprocess.check_output(['herdr','tab','list','--workspace',ws]))['result']['tabs']
panes=json.loads(subprocess.check_output(['herdr','pane','list','--workspace',ws]))['result']['panes']
by={}
for p in panes: by.setdefault(p['tab_id'],[]).append(p)
cur=os.environ.get('HERDR_TAB_ID')
for t in sorted(tabs,key=lambda t:t['number']):
ps=by.get(t['tab_id'],[]); n=len(ps)
agents = n>0 and all(p.get('agent')=='claude' for p in ps)
state = 'CANDIDATE' if (agents and n<4) else ('full' if agents else 'SKIP(not agent tab)')
print('%-6s %-22s panes=%d %-19s %s' % (t['tab_id'],(t.get('label') or '')[:22],n,state,'<-- current' if t['tab_id']==cur else ''))
"
c. Make the pane — the tab must end up a 2x2 GRID. Max 2 panes in each direction:
NEVER 3 stacked on top of each other, never 3 side by side. Split the LARGEST pane
in the target tab, not necessarily your own — splitting your own every time shreds it
(112 cols → 56 → a 17-row sliver), so the full-width/full-height pane is the one that
should give up space. Growth sequence for a tab:
- 1 pane → split it RIGHT → two columns.
- 2 panes (side by side) → split the larger one DOWN.
- 3 panes → split the remaining full-width/full-height one to complete the 2x2.
- 4 panes → full. Go place it elsewhere (step b).
Read the rects, pick the biggest, and choose the direction that does NOT put a 3rd
pane in the same row or column. Always pass --cwd <absolute path to the worktree>
so the pane starts inside it (see step a for why):
herdr pane layout --pane <any pane id in the target tab>
herdr pane split <biggest-pane-id> --direction down --cwd "<repo>/.claude/worktrees/<slug>" --no-focus
If instead every agent tab was full, create a new tab (it takes --cwd too):
herdr tab create --workspace "$HERDR_WORKSPACE_ID" --label "<slug>" --cwd "<repo>/.claude/worktrees/<slug>" --no-focus
Read the new pane id from the JSON response — result.pane.pane_id for a split,
result.root_pane.pane_id for a new tab. Call it <pane>. Never construct the id by
hand. Sanity-check the returned cwd is the worktree.
d. Label the pane, then relabel the TAB to all its pane names concatenated:
herdr pane rename <pane> "<slug>"
The pane label renders on the pane border, and on this machine the agents sidebar is
configured to show it too (see the sidebar note in another repo's feature-worktree
skill for the config details). Rebuild the tab label from the tab's panes in grid order
(top-left → bottom-right) every time you add a pane:
python -c "
import json,subprocess,os,sys
tab=sys.argv[1]
ws=os.environ['HERDR_WORKSPACE_ID']
panes=[p for p in json.loads(subprocess.check_output(['herdr','pane','list','--workspace',ws]))['result']['panes'] if p['tab_id']==tab]
lay=json.loads(subprocess.check_output(['herdr','pane','layout','--pane',panes[0]['pane_id']]))['result']['layout']
order={p['pane_id']:(p['rect']['y'],p['rect']['x']) for p in lay['panes']}
panes.sort(key=lambda p: order.get(p['pane_id'],(0,0)))
label=' | '.join((p.get('label') or '?') for p in panes)
subprocess.check_output(['herdr','tab','rename',tab,label]); print(label)
" <target-tab-id>
Note Claude Code also overwrites the pane's terminal title with an auto-generated
topic summary — that is a third string. Pane label / terminal title / tab label are all
different things; don't confuse them.
e. Launch Claude. The pane already starts in the worktree, so this is a bare command —
no cd, no chaining. Use --dangerously-skip-permissions so the delegated agent
doesn't stall on tool-permission or MCP-trust prompts:
herdr pane run <pane> 'claude --dangerously-skip-permissions'
herdr wait agent-status <pane> --status idle --timeout 120000
Then confirm the pane shows a clean prompt (herdr pane read <pane> --source recent)
before handing off — text sent while Claude is still booting is silently lost.
If you ever do need a multi-command line in a pane: panes run Windows PowerShell 5.1,
so chain with ; + if ($?), NOT && (which is a parser error).
f. Hand off the feature, and tell that agent to close its own pane when it's fully done:
herdr pane run <pane> "<the feature request, verbatim>. Build it in this worktree, commit on the <slug> branch, and push to origin if one exists. When the feature is merged into main and this worktree has been removed, close this pane with: herdr pane close \"\$HERDR_PANE_ID\""