A CLI for managing parallel agentic coding sessions. It organizes projects and git worktrees, configures per-agent environments with deterministic port allocation, and orchestrates tmux sessions where AI coding agents run independently.
It is like a shepherd, but for coding agents.
Running multiple AI coding agents in parallel requires isolated workspaces, separate environments, and session management. codeherd handles the infrastructure -- git worktrees for isolation, tmux sessions for persistence, deterministic ports to avoid conflicts -- so each agent gets a clean, independent workspace without manual setup.
codeherd manages a lifecycle that takes a project from configuration to a running agent session:
Clone -> Worktree -> File Copy -> Template Processing -> Session Start
Each step has pre/post hooks for custom automation (install dependencies, start services, notify external systems). See docs/hooks.md for the full hook lifecycle reference.
All configuration lives in ~/.config/codeherd/config.toml. Here is a full example:
[defaults]
projects_dir = "~/projects" # base directory for clones and worktrees
agent = "claude" # default agent for new sessions
# ── Agents ──────────────────────────────────────────────────────────────────
[agents.claude]
cmd = "claude"
args = ["--dangerously-skip-permissions"]
[agents.claude.env]
CLAUDE_CONFIG_DIR = "/home/user/.config/claude"
[agents.aider]
cmd = "aider"
args = ["--model", "opus"]
[agents.codex]
cmd = "codex"
args = ["--approval-mode", "full-auto"]
# ── Projects ────────────────────────────────────────────────────────────────
[projects.myapp]
repo = "git@github.com:user/myapp.git"
default_branch = "main"
# Files copied into every new worktree (see File Copy section)
files = [
"CLAUDE.md", # same path in worktree
".cursorrules", # same path in worktree
"~/.config/codeherd/prompts/safety.md:RULES.md", # absolute source, custom destination
]
# Hooks run at each lifecycle step (see Hooks section)
[projects.myapp.hooks]
post-clone = "make deps"
post-worktree = "npm install"
pre-session = "docker compose up -d"
post-session = "curl -s https://hooks.example.com/started"
[projects.api]
repo = "git@github.com:user/api.git"
default_branch = "develop"
files = [".envrc"]
[projects.api.hooks]
post-worktree = "bundle install"Each project points to a git repository and a default branch. Clone paths mirror the repo URL under projects_dir:
~/projects/
github.com/user/
myapp/ # main clone
myapp__worktrees/
feature/ # worktree for "feature" branch
fix-123/ # worktree for "fix-123" branch
api/ # another project
Agents are CLI tools configured once and selected at session start. Any command-line tool works -- Claude Code, Aider, Codex, or a custom script. Each agent defines a command, optional arguments, and optional environment variables.
Profiles let one machine carry several independent codeherd configs — e.g. a
personal and a work set of projects and agents. Enable them in the main
config and keep each profile as its own TOML file under profiles_dir:
[defaults]
profiles_enabled = true
profiles_dir = "~/.config/codeherd/profiles" # default: <config dir>/profiles
main_profile = "personal" # used when nothing else selects oneEach <profiles_dir>/<name>.toml is a full config (its own projects, agents,
projects_dir). When profiles_enabled = true, projects/agents/projects_dir
in the main config are ignored (with a warning).
The active profile is resolved by precedence, lowest to highest:
defaults.main_profilein the main config- the
CODEHERD_PROFILEenvironment variable - the
--profile/-pflag
codeherd stamps CODEHERD_PROFILE into every profile-mode session, so a nested
ch call inside a session (for example ch run <agent>) defaults to that
session's profile without needing --profile.
Sessions are tmux sessions anchored to a project and branch. They run independently and persist across disconnects:
tmux sessions:
codeherd # TUI dashboard
myapp-feature # Claude Code working on feature
myapp-fix-123 # Aider fixing a bug
api-experiment # another agent exploring an idea
codeherd stamps a fixed set of CODEHERD_* environment variables on every session it starts (both agent and shell). Use them in the agent's cmd/args or in scripts the agent invokes:
| Variable | Value | Notes |
|---|---|---|
CODEHERD_SESSION |
Canonical session name, e.g. myapp-feature |
Always set |
CODEHERD_PROJECT |
Project name from config | Always set |
CODEHERD_BRANCH |
Branch name | Always set |
CODEHERD_CLONE_DIR |
Absolute path to the main git clone | Set whenever the project has a valid repo URL. Needed by anything that runs git inside a worktree — git worktrees keep .git as a file that points back to the main clone |
CODEHERD_WORKTREE_PATH |
Absolute path to the worktree root | Always set |
CODEHERD_PROFILE |
Active profile name | Only set when a profile is active. Nested ch calls inherit it as the default profile (see Profiles) |
These values win over any conflicting keys in [agents.<name>].env — the agent-level env is applied first, then codeherd's values are stamped on top.
Example: sandbox an agent with ai-jail while keeping git operations working:
[agents.claude-sandboxed]
cmd = "ai-jail"
args = ["--rw-map", "$CODEHERD_WORKTREE_PATH", "--rw-map", "$CODEHERD_CLONE_DIR", "--", "claude"]When you create a worktree and start a session, codeherd runs through a five-step lifecycle. Each step has optional pre/post hooks.
Clone ──> Worktree ──> File Copy ──> Template Processing ──> Session Start
│ │ │ │ │ │ │ │ │ │
pre post pre post pre post pre post pre post
The files list in project config copies files into new worktrees. This is useful for shared configuration (editor rules, prompt files, env configs) that should exist in every worktree but isn't tracked in git.
| Entry format | Source | Destination |
|---|---|---|
"CLAUDE.md" |
Clone dir / CLAUDE.md |
Worktree / CLAUDE.md |
"src/config.json" |
Clone dir / src/config.json |
Worktree / src/config.json |
"~/.config/prompts/safety.md:RULES.md" |
~/.config/prompts/safety.md |
Worktree / RULES.md |
"/absolute/path/file.txt:subdir/file.txt" |
/absolute/path/file.txt |
Worktree / subdir/file.txt |
Relative paths resolve from the clone directory. Absolute paths and ~/ paths copy from the filesystem. Intermediate directories are created automatically.
After files are copied, codeherd scans the worktree for .herd files and renders them using Go's text/template engine. The rendered output is written as a sibling file without the .herd suffix:
.env.herdrenders to.envdocker-compose.yml.herdrenders todocker-compose.ymlnginx.conf.herdrenders tonginx.conf
This is how you generate per-worktree configuration with unique ports and branch-specific values.
Templates receive a context object with these fields:
| Variable | Type | Description | Example value |
|---|---|---|---|
.Project |
string | Project name from config | myapp |
.Branch |
string | Branch name | feature |
.WorktreePath |
string | Absolute path to the worktree | /home/user/projects/github.com/user/myapp__worktrees/feature |
.SessionName |
string | Derived session name | myapp-feature |
| Function | Description | Example |
|---|---|---|
port "name" |
Deterministic port (10000-59999) derived from project + branch + name. Same inputs always produce the same port, different branches get different ports. | {{ port "http" }} |
env "VAR" "default" |
Read an environment variable, with an optional fallback value. | {{ env "API_KEY" "dev-key" }} |
# .env.herd — place this in your repo or copy it via the files list
APP_PORT={{ port "http" }}
GRPC_PORT={{ port "grpc" }}
DEBUG_PORT={{ port "debug" }}
DATABASE_URL=postgres://localhost:5432/{{ .Project }}_{{ .Branch }}
API_KEY={{ env "API_KEY" "dev-key-for-local" }}
SESSION={{ .SessionName }}
Running ch create worktree myapp feature renders this to .env:
# .env (generated)
APP_PORT=34521
GRPC_PORT=18973
DEBUG_PORT=42810
DATABASE_URL=postgres://localhost:5432/myapp_feature
API_KEY=dev-key-for-local
SESSION=myapp-feature
Every worktree gets unique ports. The feature branch and fix-123 branch will never collide.
# docker-compose.yml.herd
services:
postgres:
image: postgres:16
ports:
- "{{ port "postgres" }}:5432"
environment:
POSTGRES_DB: {{ .Project }}_{{ .Branch }}
POSTGRES_PASSWORD: {{ env "PG_PASSWORD" "devpass" }}
redis:
image: redis:7
ports:
- "{{ port "redis" }}:6379"Hooks are shell commands that run at each lifecycle step. Configure them per-project:
[projects.myapp.hooks]
pre-clone = "echo preparing to clone"
post-clone = "make deps"
pre-worktree = "echo creating worktree"
post-worktree = "npm install"
pre-copy = "echo copying files"
post-copy = "chmod 600 .env"
pre-template = "vault read secret/myapp > .secrets"
post-template = "echo templates rendered"
pre-session = "docker compose up -d"
post-session = "curl -s https://hooks.example.com/started"Hooks receive context as environment variables (CODEHERD_PROJECT, CODEHERD_BRANCH, CODEHERD_WORKTREE_PATH, etc.). A non-zero exit code stops the workflow. Omit a hook to skip it.
See docs/hooks.md for the full reference including environment variables per step, working directory rules, and error handling.
mise use github:xico42/codeherd@latestOnce codeherd lands in the mise official registry, this becomes mise use codeherd@latest.
Download the appropriate archive from the latest release, extract, and place ch on your PATH. Each release ships archives for linux and darwin on amd64 and arm64, with sigstore signatures and a checksums.txt.
Requires Go 1.22+, git, and tmux.
make install # builds and installs to ~/.local/bin/chch ships dynamic completion for agents, projects, profiles, and worktree
branches. Cobra generates per-shell scripts via ch completion <shell>.
# zsh — ensure the dir is on your $fpath, then reload
ch completion zsh > "${fpath[1]}/_ch"
# bash
ch completion bash | sudo tee /etc/bash_completion.d/ch > /dev/null
# fish
ch completion fish > ~/.config/fish/completions/ch.fishCompletion respects the active profile: inside a profile-mode session
($CODEHERD_PROFILE set) or with -p <profile>, branch suggestions come
from that profile's worktrees.
# Configure a project (edit ~/.config/codeherd/config.toml)
# See the Configuration section above for the full config format
# Clone the project
ch clone project myapp
# Create a worktree and start a session
ch create session myapp feature --agent claude --attachOr use the interactive TUI:
ch| Command | Description |
|---|---|
ch |
Launch the TUI dashboard |
ch list project |
List configured projects |
ch show project <name> |
Show project details |
ch clone project <name> |
Clone a project |
ch list worktree |
List all worktrees |
ch create worktree <project> <branch> |
Create a worktree |
ch delete worktree <project> <branch> |
Delete a worktree |
ch list session |
List active sessions |
ch create session <project> <branch> |
Start an agent session (use --shell for a plain shell) |
ch attach session <project> <branch> |
Attach to a session |
ch show session <project> <branch> |
Show session details |
ch delete session <project> <branch> |
Stop a session |
ch version |
Print the installed version |
make build # build ./ch binary
make test # run unit tests
make lint # run linter
make check # coverage (80%+) + integration tests + lint + build- Project overview -- architecture, design philosophy, and roadmap
- Hooks lifecycle -- hook configuration, file copy, template processing