Skip to content

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.

A script running every 5 seconds reports the branch shown in the sidebarAn invisible command entry on the right of the tab bar runs pane-branch.sh every 5 seconds. The script reads the git branch of each agent pane and reports it with herdr pane report-metadata, and the sidebar shows it as the $branch token. The token has a 30-second TTL, so if the script stops, the label goes away.Command entryon the tab bar, every 5 spane-branch.shbranch of each agent panereport-metadataTTL 30 sSidebar $branchdisappears if it 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.

~/.config/herdr/config.toml
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

~/.config/herdr/pane-branch.sh
#!/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

  1. 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.
  2. 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)}'