Herdr
herdr, where every agent session runs — a sidebar that marks only agents waiting on me, /team worker panes, a branch token, and account switching.
herdr is an agent multiplexer. Like tmux it splits tabs and panes, but its sidebar also shows which agent runs in each pane and whether it is working or waiting for my answer. I run every agent session in it.
The captions along the bottom name the Cmd key first and the matching prefix key in brackets.
How it works
The sidebar marks only the panes I need to look at
With several agents running, the question is who is waiting on me. herdr's sidebar shows each pane's agent and state, and I set the state text to show only for blocked (waiting for my answer) and done. Panes that are working stay quiet, and only the ones I need to touch carry text. Branches follow the same idea: default branches (main, master, develop) fade out, and working branches take a colour by prefix (feat, fix, hotfix).
/team workers start in here too
When I type /team, the lead session starts worker panes inside this herdr itself (herdr agent start). The workers know the names of the lead and of each other from their first prompt, and closing the herdr tab ends the Claude workers too. The lead reads worker screens through a script (team.sh peek) and treats a worker that has been busy for more than 30 minutes as stuck and looks in.
A timer script fills in the sidebar branch
Each agent works in its own worktree, so the sidebar has to show which pane works on which branch. To show that branch on the second line of an agent row, I use a custom token, $branch. herdr does not fill this token itself. So the right side of the tab bar has a command entry that prints nothing, and it runs pane-branch.sh every 5 seconds. The script reads the branch of each agent pane and reports it with herdr pane report-metadata. The token has a 30-second TTL, so the branch disappears on its own if the script stops.
One key switches the account
Cmd+Shift+A calls tak-bro.account-switch, a herdr plugin I wrote. It swaps Claude Code and Codex login profiles with one key, and sessions already running pick up the new account within 30 seconds. When one account's usage window fills up, I move to another without logging in again in a browser. A $acct badge on each pane shows which account it uses.
My config
herdr config
The sidebar has two panels. The spaces panel on top shows projects and branches. The agents panel below leads with who is running. Each kind of agent gets its own colour.
onboarding = false
[theme]
name = "dracula"
auto_switch = false
# Sidebar sits one step darker than the Ghostty background (#1D1E27) so it
# reads as its own panel. Dracula's neon state colours are swapped for pastels
# so state icons and the sidebar row colours below share one palette.
[theme.custom]
accent = "#c6a0f6"
green = "#a6da95"
yellow = "#eed49f"
red = "#ed8796"
blue = "#8aadf4"
teal = "#8bd5ca"
peach = "#f5a97f"
mauve = "#c6a0f6"
sidebar_bg = "#17181f"
active_row_bg = "#2a2c3a"
selection_bg = "#44475a"
[ui]
agent_panel_sort = "spaces"
status_indicators = "symbols"
prompt_new_tab_name = false
# The command entry prints nothing and so never shows: it is the timer that
# keeps the sidebar's $branch token fresh (see pane-branch.sh).
tab_bar_right = [
{ type = "zoom" },
{ type = "datetime", format = "%H:%M" },
{ type = "command", command = "~/.config/herdr/pane-branch.sh", interval_seconds = 5, timeout_seconds = 3 },
]
tab_bar_right_separator = " · "
# The spaces panel above already lists projects, so agent rows lead with who:
# line 1 is the agent, coloured per kind, plus the state in words only when it
# needs me (blocked, done) — the icon already carries idle and working.
# Line 2 is the workspace and, off the default branch, the branch without its
# type prefix (reported by pane-branch.sh), both faded.
[ui.sidebar.agents]
row_gap = 1
rows = [
[
"state_icon",
{ token = "agent", fg = "#8aadf4", bold = true, rules = [
{ equals = "hwip cc", fg = "#f5a97f" },
{ contains = "claude", ignore_case = true, fg = "#f5a97f" },
{ equals = "hwip pi", fg = "#c6a0f6" },
{ equals = "pi", fg = "#c6a0f6" },
{ contains = "codex", ignore_case = true, fg = "#8bd5ca" },
] },
{ token = "state_text", rules = [
{ starts_with = "blocked", fg = "#ed8796", bold = true },
{ starts_with = "done", fg = "#a6da95" },
{ starts_with = "", hide = true },
] },
],
[
{ token = "workspace", fg = "#6e738d", bold = false },
{ token = "$branch", fg = "#6e738d" },
],
]
# Default branches fade out; working branches take a colour by prefix.
[ui.sidebar.spaces]
row_gap = 1
rows = [
["state_icon", { token = "workspace", fg = "#e4e6f5", bold = true }],
[
{ token = "branch", fg = "#8aadf4", rules = [
{ equals = "main", fg = "#6e738d" },
{ equals = "master", fg = "#6e738d" },
{ equals = "develop", fg = "#6e738d" },
{ starts_with = "feat", fg = "#a6da95" },
{ starts_with = "fix", fg = "#f5bde6" },
{ starts_with = "hotfix", fg = "#ed8796" },
] },
"git_status",
],
]
[ui.sound]
enabled = true
[keys]
prefix = "ctrl+space"
# tabs
new_tab = ["prefix+c", "cmd+t"]
close_tab = ["prefix+shift+x", "cmd+shift+w"]
previous_tab = ["prefix+p", "cmd+shift+["]
next_tab = ["prefix+n", "cmd+shift+]"]
switch_tab = ["prefix+1..9", "cmd+1..9"]
# panes
split_vertical = ["prefix+v", "cmd+d"]
split_horizontal = ["prefix+minus", "cmd+shift+d"]
close_pane = ["prefix+x", "cmd+w"]
focus_pane_left = ["prefix+h", "cmd+alt+left"]
focus_pane_down = ["prefix+j", "cmd+alt+down"]
focus_pane_up = ["prefix+k", "cmd+alt+up"]
focus_pane_right = ["prefix+l", "cmd+alt+right"]
cycle_pane_next = ["prefix+tab", "cmd+]"]
cycle_pane_previous = ["prefix+shift+tab", "cmd+["]
zoom = ["prefix+z", "cmd+shift+enter"]
# Rectangle defaults bind cmd+ctrl+left/right to topLeft/topRight; macos-defaults.sh clears them.
resize_pane_left = "cmd+ctrl+left"
resize_pane_down = "cmd+ctrl+down"
resize_pane_up = "cmd+ctrl+up"
resize_pane_right = "cmd+ctrl+right"
# workspaces
new_workspace = ["prefix+shift+n", "cmd+shift+n"]
goto = ["prefix+g", "cmd+k"]
previous_workspace = "cmd+shift+up"
next_workspace = "cmd+shift+down"
switch_workspace = ["prefix+shift+1..9", "alt+1..9"]
toggle_sidebar = ["prefix+b", "cmd+b"]
[[keys.command]]
key = "cmd+shift+g"
type = "popup"
command = "lazygit"
description = "lazygit"
width = "90%"
height = "90%"
# Account picker (plugin tak-bro.account-switch): swaps the Claude Code / Codex login.
[[keys.command]]
key = "cmd+shift+a"
type = "shell"
command = "herdr plugin action invoke open --plugin tak-bro.account-switch"
description = "accounts"
Branch token script
#!/bin/sh
# Reports each agent pane's git branch as its `branch` metadata token, which
# the sidebar agent rows in config.toml show as `$branch`.
#
# herdr runs this from a `tab_bar_right` command entry purely as a timer: it
# prints nothing, so the entry itself stays invisible. The token carries a TTL,
# so it disappears on its own if this stops running.
#
# The type prefix is dropped (feat/herdr-sidebar -> herdr-sidebar) and
# the default branches config.toml fades (main, master, develop) report nothing:
# the spaces panel already shows the full branch.
PATH="/opt/homebrew/bin:/usr/local/bin:$PATH"
herdr=${HERDR_BIN_PATH:-herdr}
tab=$(printf '\t')
"$herdr" pane list |
jq -r '.result.panes[] | select(.agent != null) | [.pane_id, .foreground_cwd // .cwd] | @tsv' |
while IFS=$tab read -r pane cwd; do
branch=$(git -C "$cwd" symbolic-ref --quiet --short HEAD 2>/dev/null)
case $branch in
main | master | develop) branch= ;;
esac
# An empty value clears the token.
"$herdr" pane report-metadata "$pane" --source custom:pane-branch \
--token "branch=${branch#*/}" --ttl-ms 30000 >/dev/null
done
Check
- Start an agent in a repo that is on a working branch. Within 5 seconds, the second line of its sidebar row shows the branch name without its type prefix.
- If no branch shows, look at the values the script reads.
herdr pane list | jq '.result.panes[] | select(.agent != null) | {pane_id, cwd: (.foreground_cwd // .cwd)}'