hrd

Herd your repos. Run commands across them in parallel. Watch results stream in live.

hrd is a multi-repo manager for developers who work across many repositories and use both git and jj (Jujutsu). It keeps your repos organized into groups, runs VCS commands across all of them at once, and shows a live unified status dashboard — with full awareness of branches, bookmarks, remote tracking, ahead/behind counts, and conflicts.

CI

Features

Install

Homebrew (macOS/Linux)

brew install hugoh/tap/hrd

Linux (deb/rpm)

Download the .deb or .rpm from the releases page and install with your package manager:

# Debian/Ubuntu
sudo apt install ./hrd_*.deb

# RHEL/Fedora
sudo dnf install ./hrd_*.rpm

mise

mise use -g github:hugoh/hrd

Go install

go install github.com/hugoh/hrd@latest

From source

git clone https://github.com/hugoh/hrd
cd hrd
go build -o hrd .

Quick start

# Track some repos
hrd repo add ~/dev/myproject ~/dev/infra
hrd repo add -n dotfiles ~/.local/share/chezmoi

# Or discover everything under a directory at once
hrd repo scan add ~/dev
hrd repo scan add -g work ~/work     # ...and group what it finds

# Or keep a directory always in sync — new repos show up with no re-scan
hrd repo root add ~/my-repos -g personal

# Start the TUI
hrd

# Add repos to groups
hrd group add work myproject infra

# Live status across all repos in context
hrd ls

# Run a command across all repos
hrd fetch

# Or just the ones you care about right now
hrd jj dotfiles log

# Arbitrary shell commands
hrd shell -- 'echo $(basename $PWD): $(git rev-parse --short HEAD)'

Tip: Group names are displayed with an @ prefix (e.g., @work, @oss) to distinguish them from repo names. The @ is optional on input — hrd ls @work and hrd ls work both work. @@none is a reserved pseudo-group matching repos with no group at all (e.g. hrd ls @@none) — the double @ keeps it from ever colliding with a real group name, which you also can’t create (group names can’t start with @).

Status dashboard

NAME        VCS STATUS                                                   
myproject   git main ↑2*  feat: add new feature (2 hours ago)            
dotfiles    jj  main ✓∅⇡5  config: update zshrc (1 day ago)              
infra       git feat/rework ↑1↓3  refactor: networking layer (3 days ago)
old-service jj  legacy ✗✓!‼  fix: critical bug (1 week ago)

Status symbols at a glance:

Symbol Meaning
Synced with remote
↑N N commits ahead of remote
↓N N commits behind remote
⇡N Working copy ahead of bookmark (local)
↑N↓N Diverged (ahead and behind)
Local only, no remote
Unresolved conflict
! Bookmark conflict (jj)
Remote was deleted
* Dirty working copy
? Unknown remote state

Interactive TUI

Run hrd (or hrd tui) to open the full-screen terminal UI:

The TUI mirrors the CLI: the same backends, the same parallel execution, the same status parsing — just in an interactive, always-on view.

Command reference

hrd manages a set of git and jj repositories tracked in a config file,
letting you run status checks and VCS operations across many of them at once.

Run with no subcommand to open the TUI, optionally scoped to specific repos or
groups. -c/--config applies to every subcommand and defaults to
$XDG_CONFIG_HOME/hrd/config.toml, falling back to ~/.config/hrd/config.toml.

Usage:
  hrd [flags]
  hrd [command]

Examples:
  hrd                    # open the TUI across all tracked repos
  hrd myrepo             # open the TUI scoped to one repo
  hrd repo add ~/code/*  # start tracking repos
  hrd ls                 # show status of all tracked repos

Repository management:
  group       manage repo groups
  repo        manage tracked repositories

Inspect:
  diff        show diff for repos (git diff or jj diff)
  ll          show status of repos with commit message and time
  log         show log for repos (git log or jj log)
  ls          show status of repos
  status      show detailed status for repos (git status or jj status)

Remote sync:
  fetch       fetch from remotes (git fetch or jj git fetch)
  pull        pull from remotes (git pull or jj git pull)
  push        push to remotes (git push or jj git push)

Run commands:
  git         run a git command across repos
  jj          run a jj command across repos
  shell       run an arbitrary shell command across repos
  tui         interactive terminal UI for browsing and running commands across repos

Additional Commands:
  completion  Generate the autocompletion script for the specified shell
  help        Help about any command

Flags:
  -c, --config string   path to config file (default "~/.config/hrd/config.toml")
  -h, --help            help for hrd
  -v, --version         version for hrd

Use "hrd [command] --help" for more information about a command.

Scoping and --

The git, jj, and shell commands take an optional repo/group scope followed by the command to run. Everything after -- is passed to the subprocess verbatim — repo names in the command are never reinterpreted as scope:

hrd git myrepo @work -- log --oneline -5
hrd shell -- grep -r TODO .

Without --, the leading args that match repo or group names form the scope and the first non-matching arg starts the command (hrd git myrepo log works, but flags like --oneline need the -- form).

Repo discovery

hrd repo scan add <dir>... walks each directory (default depth 5, tune with --depth) and tracks every git/jj repo it finds as its own entry in the config. Detected repos are not descended into, so vendored or nested checkouts stay untracked; hidden directories are skipped. -g/--group assigns everything found to a group. Name collisions fall back to <parent>-<dir>; already-tracked paths are left alone, so re-running scan add is safe. hrd repo scan ls <dir>... previews the same walk without saving — purely read-only — with --tracked/--untracked to filter by whether a match is already in the config.

hrd repo scan add ~/dev --depth 2 --pattern 'api-*' -g backend
hrd repo scan ls ~/dev --untracked

For a directory you want to stay in sync automatically, use hrd repo root add <dir> instead: it’s a standing declaration, walked live on every hrd invocation, so repos added under it later show up with no re-scan and nothing is written to the config for the discovered repos themselves — only the root is persisted. --depth (default 1) caps how deep it walks and -g/--group tags every repo found under it.

hrd repo root add ~/my-repos --depth 2 -g personal
hrd repo root ls
hrd repo root rm my-repos

Status filters

ls, ll, the VCS subcommands (status, diff, log, fetch, pull, push), shell, git, and jj accept status filters that narrow the scope to repos in a given state (multiple flags are a union):

hrd push --ahead          # push only repos with unpushed commits
hrd pull --behind @work   # pull only the work repos that are behind
hrd ls --dirty --names    # script-friendly list of dirty repos
hrd shell --dirty -- git stash list
Flag Matches repos that…
--dirty have uncommitted changes in the working copy
--ahead are ahead of their remote, or have local-only work
--behind are behind their remote

Aliases

Define your own commands in the config and they become first-class subcommands — with repo/group scoping, --interactive, status filters, and shell completion, and listed under hrd --help:

[aliases]
sync = "pull --rebase"
mkclean = "!make clean"
hrd sync @work          # git pull --rebase / jj git pull, per backend
hrd mkclean --dirty     # make clean, only in dirty repos
hrd sync -- --autostash # extra args append to the expansion

The first word decides the routing: git/jj pins a backend, ! (or sh) runs a shell command, anything else routes through each repo’s own backend like the built-in subcommands. The TUI command bar completes and expands the same aliases. Aliases that would shadow a built-in command are ignored with a warning.

Exit codes

Code Meaning
0 All repos succeeded
1 The command ran but failed in at least one repo
2 Usage or config error (unknown repo, bad flags)

Configuration

Config lives at ~/.config/hrd/config.toml (respects $XDG_CONFIG_HOME).

[repos.dotfiles]
path = "/home/alice/.local/share/chezmoi"

[repos.myproject]
path = "/home/alice/dev/myproject"
groups = ["work"]

[repos.infra]
path = "/home/alice/dev/infra"
groups = ["work"]

[roots.personal]
path = "/home/alice/my-repos"
depth = 1
groups = ["personal"]

[settings]
concurrency = 8

[aliases]
sync = "pull --rebase"          # per-repo routing (git pull / jj git pull)
gpf = "git push --force-with-lease"  # always that backend
mkclean = "!make clean"         # "!" or "sh " prefix = shell command

Note: Groups are derived from the groups field on each repo, plus the groups field on any [roots.*] entry (inherited by every repo discovered under it). Group names are displayed with an @ prefix (e.g., @work) to distinguish them from repo names. The @ is optional on input — work and @work are treated identically. Group names can never start with @ (so hrd group add @work myrepo stores work, not @work) — this reserves the @@ namespace (e.g. @@none, see the Quick start tip) for built-in pseudo-groups, which can never collide with anything you create.


Contributing

Contributions are very welcome! Please open an issue or submit a pull request. See development instructions.

Adding a backend

Implement the Backend interface in a new package, add a Register() function that calls backend.Register(), and call it from main.go’s Run() function. The interface is four methods: Name, Detect, Status, and Run.


gita is the direct inspiration for hrd.

Jujutsu (jj) VCS motivated creating hrd with first-class non-git support.

Commands

hrd

manage multiple git and jj repositories

hrd manages a set of git and jj repositories tracked in a config file, letting you run status checks and VCS operations across many of them at once. Run with no subcommand to open the TUI, optionally scoped to specific repos or groups. -c/--config applies to every subcommand and defaults to $XDG_CONFIG_HOME/hrd/config.toml, falling back to ~/.config/hrd/config.toml.

-c, --config
path to config file (default: ~/.config/hrd/config.toml)

hrd diff

show diff for repos (git diff or jj diff)

--ahead
only repos ahead of their remote (or with local-only work) (default: false)
--behind
only repos behind their remote (default: false)
--dirty
only repos with a dirty working copy (default: false)
-i, --interactive
run with a real terminal (sequential, one repo at a time) (default: false)

hrd fetch

fetch from remotes (git fetch or jj git fetch)

--ahead
only repos ahead of their remote (or with local-only work) (default: false)
--behind
only repos behind their remote (default: false)
--dirty
only repos with a dirty working copy (default: false)
-i, --interactive
run with a real terminal (sequential, one repo at a time) (default: false)

hrd git

run a git command across repos

Runs "git <args>" in each scoped repo. Everything before "--" selects the scope (repo/group names, or -r/--repos); everything after "--" is passed to git verbatim. With no scope, all tracked repos are used. Only repos using the git backend are affected; others are skipped. Use -i/--interactive to run sequentially with a real terminal instead of in parallel.

--ahead
only repos ahead of their remote (or with local-only work) (default: false)
--behind
only repos behind their remote (default: false)
--dirty
only repos with a dirty working copy (default: false)
-i, --interactive
run with a real terminal (sequential, one repo at a time) (default: false)

hrd group

manage repo groups

--list-reserved
list reserved "@@" pseudo-groups and what they mean (default: false)

hrd group add

add one or more repos to a group

hrd group ls

list groups

--live
also compute repo membership for reserved groups that require live git/jj status (e.g. @@attention) (default: false)

hrd group rm

remove one or more repos from a group

hrd jj

run a jj command across repos

Runs "jj <args>" in each scoped repo. Everything before "--" selects the scope (repo/group names, or -r/--repos); everything after "--" is passed to jj verbatim. With no scope, all tracked repos are used. Only repos using the jj backend are affected; others are skipped. Use -i/--interactive to run sequentially with a real terminal instead of in parallel.

--ahead
only repos ahead of their remote (or with local-only work) (default: false)
--behind
only repos behind their remote (default: false)
--dirty
only repos with a dirty working copy (default: false)
-i, --interactive
run with a real terminal (sequential, one repo at a time) (default: false)

hrd ll

show status of repos with commit message and time

--ahead
only repos ahead of their remote (or with local-only work) (default: false)
--behind
only repos behind their remote (default: false)
-d, --dirs
show repo dirs only, one per line (default: false)
--dirty
only repos with a dirty working copy (default: false)
-m, --message
show commit message and time (default: false)
-n, --names
show repo names only, one per line (default: false)

hrd log

show log for repos (git log or jj log)

--ahead
only repos ahead of their remote (or with local-only work) (default: false)
--behind
only repos behind their remote (default: false)
--dirty
only repos with a dirty working copy (default: false)
-i, --interactive
run with a real terminal (sequential, one repo at a time) (default: false)

hrd ls

show status of repos

--ahead
only repos ahead of their remote (or with local-only work) (default: false)
--behind
only repos behind their remote (default: false)
-d, --dirs
show repo dirs only, one per line (default: false)
--dirty
only repos with a dirty working copy (default: false)
-m, --message
show commit message and time (default: false)
-n, --names
show repo names only, one per line (default: false)

hrd pull

pull from remotes (git pull or jj git pull)

--ahead
only repos ahead of their remote (or with local-only work) (default: false)
--behind
only repos behind their remote (default: false)
--dirty
only repos with a dirty working copy (default: false)
-i, --interactive
run with a real terminal (sequential, one repo at a time) (default: false)

hrd push

push to remotes (git push or jj git push)

--ahead
only repos ahead of their remote (or with local-only work) (default: false)
--behind
only repos behind their remote (default: false)
--dirty
only repos with a dirty working copy (default: false)
-i, --interactive
run with a real terminal (sequential, one repo at a time) (default: false)

hrd repo

manage tracked repositories

hrd repo add

add one or more repositories

-g, --group
add the repo(s) to this group
-n, --name
explicit name (only valid when adding a single repo)

hrd repo ls

list tracked repositories, optionally filtered to one group

hrd repo rename

rename a repository

hrd repo rm

remove one or more repositories

hrd repo root

manage live-resolved directory roots

hrd repo root add

track one or more directories to walk for repos on every invocation

Registers each dir as a live-resolved root: every hrd invocation walks it (up to --depth levels deep, default 1) looking for git and jj repositories and merges what it finds into the working repo set. Nothing is written to config.toml for the discovered repos themselves — only the root is persisted, so new repos added under dir appear automatically.

--depth
maximum directory depth to walk on every invocation (default: 1)
-g, --group
assign repos discovered under these roots to a group
-n, --name
explicit name (only valid when adding a single root)

hrd repo root ls

list tracked directory roots

hrd repo root rm

stop tracking one or more directory roots

hrd repo scan

discover repositories under one or more directories

hrd repo scan add

discover and add repositories under one or more directories

Walks each given directory (up to --depth levels deep) looking for git and jj repositories, and adds every match to the config under its directory name. Use --pattern to filter by name and -g/--group to assign found repos to a group. -i/--confirm prompts before adding each repo instead of adding all matches unconditionally.

-i, --confirm
prompt before adding each repo (default: false)
--depth
maximum directory depth to descend (default: 5)
-g, --group
add found repos to this group
-p, --pattern
glob pattern matched against repo directory name to filter results

hrd repo scan ls

list repositories discovered under one or more directories

Walks each given directory (up to --depth levels deep) looking for git and jj repositories and prints what it finds. Purely read-only — never modifies the config. Use --tracked/--untracked to filter by whether a match is already in the config; use "repo scan add -g" to track and group in one step.

--depth
maximum directory depth to descend (default: 5)
-p, --pattern
glob pattern matched against repo directory name to filter results
--tracked
show only repos already in config (default: false)
--untracked
show only repos not yet in config (default: false)

hrd shell

run an arbitrary shell command across repos

Runs a command via "sh -c" in each scoped repo's directory. Everything before "--" selects the scope (repo/group names, or -r/--repos); everything after "--" is the command. Pass it as one quoted string to keep shell syntax like pipes and redirection working, or as separate words to have them rejoined with automatic quoting. Use -i/--interactive to run sequentially with a real terminal instead of in parallel.

--ahead
only repos ahead of their remote (or with local-only work) (default: false)
--behind
only repos behind their remote (default: false)
--dirty
only repos with a dirty working copy (default: false)
-i, --interactive
run with a real terminal (sequential, one repo at a time) (default: false)

hrd status

show detailed status for repos (git status or jj status)

--ahead
only repos ahead of their remote (or with local-only work) (default: false)
--behind
only repos behind their remote (default: false)
--dirty
only repos with a dirty working copy (default: false)
-i, --interactive
run with a real terminal (sequential, one repo at a time) (default: false)

hrd tui

interactive terminal UI for browsing and running commands across repos