A herdr plugin that sends your branch — as it sits on disk right now, uncommitted changes and all — into a fresh E2B cloud sandbox. No push, no clone, no credentials but the ones you put there. Works from any checkout; a herdr worktree additionally tears its sandbox down when the worktree is removed.
1 · Delegate this branch. You are on a branch, you want the work done
somewhere that isn't your laptop. e2b-box uploads the checkout, boots a box
from a template that already ships a coding agent, and drops you in its shell.
pull brings the diff home.
2 · Race a fleet, then grade it. One task, one box per agent, all at once —
Claude Code, Codex, Grok, OpenCode, Amp, Droid, Prime and Muse Code work the same starting
point in parallel, and e2b-bench runs one held-out check inside every box to
say which of them actually did it. Best-of-N, or one harness's feature you can't
get from the others.
herdr plugin install e2b-dev/herdr-e2b-sandbox # build step links e2b-box onto PATH
./install.sh # prompts for your E2B API key, chmod 600Needs herdr ≥ 0.7.0, Node ≥ 22, jq, the e2b CLI on PATH, and an
E2B API key. The key goes in [secrets].e2b_api_key
in the plugin config (~/.config/herdr/plugins/config/e2b-dev.herdr-e2b/config.toml), or
in E2B_API_KEY, which wins if both are set.
On first run the installer also runs e2b-box auth once — it reports which coding
harnesses are installed on this machine and records the credentials a box may
borrow, so your first box comes up authenticated instead of on the agent's own
sign-in screen. It asks for no harness credential and cannot fail the install: a
machine with no harnesses installed just gets a report. Install a new harness
later and re-run e2b-box auth yourself; nothing probes on its own.
e2b-box auth opens an interactive table in a terminal: use arrows or j/k,
numbers to jump, and Enter for agent actions. Connect/reconnect Claude or Codex,
inspect authentication, or open the config files. f opens API-key/template config,
a opens generated auth.toml, s saves discovery after confirmation, r refreshes,
and q/Esc exits without saving. File opening uses [dashboard].config_opener.
auth discover retains the report-and-save prompt; auth --yes saves without a
menu. Piped output remains a compact ASCII table with agent, auth method (OAuth token,
OAuth session, or API key), source, and status. The overview combines saved connections,
manual configuration, and discovered credentials, and shows setup steps only
where needed. Missing, expired, or ambiguous selected connections require attention;
the report never substitutes a lower-priority key. “Configured” describes local
credential availability, not verified authentication inside a box. Saving discovery
updates only auth.toml; your config and connections stay unchanged. Use
auth explain --template NAME for paths and account details. Narrow terminals
omit secondary columns; TTY colors respect NO_COLOR.
For a named connection you explicitly choose for new boxes:
e2b-box auth connect claude --name work # browser approval, token captured privately
e2b-box auth connect codex # borrow your local subscription session
e2b-box auth connect muse # sign in with Meta; the Muse API key it mints is stored
e2b-box auth connect amp # opens amp's Security page; paste its access token, stored privately
e2b-box auth connect codex --oauth # a second ChatGPT login owned by the plugin (ADR 0015)
e2b-box auth list
e2b-box auth explain --template claude
e2b-box up -t claude --connection workClaude's local subscription type and organization are detected automatically;
Team/Enterprise membership does not enable sharing. Connections currently have
personal access. --org requires a future shared service. Local account metadata
is labeled separately because a browser authorization can select another account.
Without --name, a detected Team/Enterprise organization suggests a name such as
claude-e2b; Pro/Max suggests claude-personal, and an unknown plan suggests
claude-local. A Team/Enterprise plan without a usable organization name uses
claude-team or claude-enterprise. The preview and list show Local account
(for example, Claude Team · E2B) separately from Access: Only you. Existing IDs
stay unchanged, including when reconnected; use the ID shown by auth list.
Interactive Claude capture requires Python 3 and the Claude CLI. An existing
setup-token can also be supplied through auth connect claude --token-stdin --yes
using a secret manager or another private pipe. Never put a token in command
arguments. Metadata and token files live under $CONFIG_DIR/connections/, protected
by directory/file permissions (0700/0600); the token files are not encrypted.
auth check ID checks local availability and known expiry without making a model
request. auth refresh does the same for every connection and every discovered
session at once (an ID narrows it), prints how long each has left, and exits 1
when a row would leave a new box unauthenticated. The only thing it rotates is a
codex connection made with --oauth, whose refresh chain this plugin alone holds
(ADR 0015), and only once the bearer is inside its last five days; every other row
is a report, and no running box is touched. It is safe to run from cron.
auth reconnect ID replaces the local credential; auth disconnect ID removes it.
Existing boxes keep their credentials until explicitly recreated.
Codex borrowing excludes the real refresh token and lasts until bearer expiry.
The only connection for a harness becomes its default. With several connections
(auth connect claude --name work), choose --connection work, or set
connection = "work" under [templates.claude] in config.toml. A selected
connection removes competing auth environment variables. If it is unavailable,
the command fails instead of selecting another billing account. Without any named
connection, the existing discovery/config precedence continues to apply.
auth discover is an explicit alias for the original auth command.
For a custom template such as drew-claude, explicitly choose its connection:
e2b-box up -t drew-claude --connection claude-personal, or add this to config.toml:
[templates.drew-claude]
connection = "claude-personal"That binding also selects the harness's onboarding seed, unless the template has
its own [fleet.seed] override. Template names are never guessed from substrings.
Bind the three verbs you press (prefix+e is herdr's own edit_scrollback —
stay off it):
[[keys.command]]
key = "prefix+shift+e" # one box, this checkout
command = "herdr plugin action invoke open --plugin e2b-dev.herdr-e2b"
[[keys.command]]
key = "prefix+shift+f" # a fleet, off this checkout
command = "herdr plugin action invoke fleet --plugin e2b-dev.herdr-e2b"
[[keys.command]]
key = "prefix+shift+p" # pull the box's changes down
command = "herdr plugin action invoke pull --plugin e2b-dev.herdr-e2b"
[[keys.command]]
key = "prefix+shift+d" # the board, every box
command = "herdr plugin action invoke dashboard-toggle --plugin e2b-dev.herdr-e2b"prefix+shift+e opens the template picker and sandbox shell in a new pane below
the invoking pane. prefix+shift+f opens fleet creation below it, then shows the
dashboard there. Both focus the new pane, like prefix+-.
prefix+shift+p is herdr's own rename_pane out of the box; move it first
(rename_pane = "alt+r" in the [keys] table) or the pull never fires. The pull
reports as a herdr notification and refuses to overwrite uncommitted local edits,
naming the files when it does.
prefix+shift+d is herdr's own close_workspace out of the box — this takes it
over. Keep that verb by moving it first (close_workspace = "prefix+shift+x" in
the [keys] table), or give the board a different key.
Local dev:
herdr plugin link /path/to/herdr-e2b-sandbox && ./install.sh.
cd ~/some/checkout
e2b-box # pick a template, boot, land in the box's shell
e2b-box exec 'npm test' # …or run one command and get JSON back
e2b-box pull # bring the box's changes down
e2b-box run -t claude --task GOAL.md --push --json # headless: task in, branch out
e2b-fleet # name a task, tick a roster, watch the board
e2b-bench login-fix --grade 'npm test'Nothing goes to the cloud on its own — you decide which checkouts go up:
A template ships the agent, not its credential. Give each template the key its
own agent reads and every box boots ready to work — keyed by template, so a
base box gets nothing:
[templates.claude.env]
CLAUDE_CODE_OAUTH_TOKEN = "sk-ant-oat01-…" # `claude setup-token`, on a subscription
# ANTHROPIC_API_KEY = "sk-ant-…" # …or a Console key, if that is what you have
[templates.codex.env]
OPENAI_API_KEY = "sk-…"
[sandbox.env] # optional — every box, whatever it booted from
HTTPS_PROXY = "http://proxy.internal:3128"
[templates.opencode] # optional: which model, how hard it thinks
model = "openrouter/qwen/qwen3.8-max-0902"
[templates.codex]
model = "gpt-5.4"
reasoning = "high"model and reasoning pin what a template's agent runs, for every box booted
from it (open, fleet, run), in the harness's own way: claude and opencode
read a variable, codex, grok, droid, prime and muse get their config file written
right after the first-run seed. An agent left alone runs whatever its provider
ranks first, which on an OpenRouter key was an image model. Per-harness keys and
what each can pin are in config.example.toml.
| Agent | Template | Key it reads | Started unattended as |
|---|---|---|---|
| Claude Code | claude |
CLAUDE_CODE_OAUTH_TOKEN (a claude setup-token token, on a Pro/Max/Team subscription) or ANTHROPIC_API_KEY (a Console key) |
claude --dangerously-skip-permissions |
| Codex | codex |
OPENAI_API_KEY |
codex --dangerously-bypass-approvals-and-sandbox |
| Grok | grok |
XAI_API_KEY |
grok --always-approve |
| OpenCode | opencode |
whichever provider key you point it at | opencode --auto --prompt |
| Amp | amp |
AMP_API_KEY |
amp --dangerously-allow-all |
| Droid | droid |
FACTORY_API_KEY |
droid |
| Prime | prime |
PRIME_API_KEY, or an ANTHROPIC_API_KEY / OPENAI_API_KEY you already pay for |
prime-agent |
| Muse Code | muse |
META_API_KEY (a Meta API key; it outranks the browser login, which sits in the Keychain and is not borrowed) |
muse --yolo |
| — | base |
— | nothing (a control arm) |
These are E2B's public agent templates; names are
per region (e2b template list), and one that isn't built in yours falls back
to base and says so. Keys are passed to Sandbox.create at create time
only — never baked into an image, never written to the box record or the log.
Rotating one means e2b-box kill and open again.
Three things follow from a fleet member being unattended:
- Skip-approval flags are the default, because nobody is watching the pane
to approve an edit. Right in a disposable box, wrong on a laptop. A box is
isolated but not a cage — it has network egress and holds your key, so the
protection is a short-lived box and a scoped key. Override per template in
[fleet.agents];""means "plain shell, start nothing". - First-run state is seeded just before the agent is typed, so a member
arrives at a prompt instead of a welcome wizard —
~/.claude.json,~/.codex/auth.json, droid's trusted folders and default autonomy, opencode's autoupdate. The command sent to the pane names the variable, never its value, so nothing secret lands in the scrollback. Shipped forclaude,codex,droidandopencode; add your own with[fleet.seed]. config.tomlis a plaintext secret file.chmod 600it.
--template NAME (or E2B_TEMPLATE) decides what a new box boots from; with
neither, a [[sandbox.template_rules]] branch pattern decides; with no rule
either, you get the chooser:
E2B template for my-worktree
▸ [1] muse high default
[2] claude high
[3] codex ultra
[4] opencode default (pick in opencode's UI)
[5] amp ultra
[6] grok xhigh
[7] droid max
[8] prime max
[9] base
↑/↓ move →/tab effort column enter confirm (on the effort cell: its list) number jumps t type a name q/esc abort
The next two cells are which model that box's agent runs and how hard it
thinks, both in the harness's own words: src/effort.js reads the model list and
the effort scale off src/harness-catalog.generated.js, which
npm run catalog -- --write reads off the CLI in each template. Focus is a cell:
→/tab moves it onto the model column, then the effort column; enter on a cell opens
the row's list (↑/↓ choose, enter takes, esc keeps; a long model list scrolls), and
the focus stays on the cell you just set, so what changed is what is highlighted;
←/shift-tab back to the template column and enter confirms. default
leaves the [templates.<name>] model / reasoning pin or the harness default
alone; a cell opens on its pin when there is one. A model cell appears only where
the plugin can deliver the pick (claude, codex, grok, droid, prime); amp has no model
setting and opencode's universe is all of models.dev, so those rows have none.
The screen redraws in place, never clears, so moving around does not flash. From
the open keybinding the picker appears in the popup, centered; enter closes it and
boots the box in a pane below the one you pressed the key in. Tab walks every cell
and then the next row, and shift-tab walks the same path backwards (off a row's
first column onto the last cell of the row above), so one key visits the whole
table in either direction. The same choice from a
script is e2b-box open -t grok --model grok-4.6 --reasoning xhigh; a model the harness
does not list, or an effort that model does not take (grok-4.5 has no xhigh,
droid's minimax-m3 takes high alone), fails the box before anything is created,
naming what it does accept. opencode's reasoning
is a per-model variant chosen in its UI, so its cell is a generic scale that only
reaches the box as HERDR_E2B_REASONING; amp's is its --mode.
t takes any name, so the menu is a shortcut and not a whitelist. muse (Meta's
Muse Code) is the shipped default; base is E2B's minimal image, fine for trying
the flow or a plain shell, tight on disk for real work. For that, build a custom template with your
toolchain and roomier resources, and point [sandbox].template at it.
Regions are named, not spelled as hosts.
[sandbox] region = "us" | "eu"is the whole surface; there is nodomainkey.usis the default and resolves tohttps://api.e2b.app,eutohttps://api.e2b-juliett.dev. US also answers ate2b.dev— an older name for the same environment — which is exactly why the plugin picks the host and you pick the region.
A template you build lands in your E2B project, and E2B names it
<project>/<template> — ondrejs-project/herdr-agents. Use that whole name
wherever a template name goes: --template, [sandbox] template, the roster,
the t prompt above. The plugin never composes the project half for you and
never turns a bare name into a namespaced one, so write it out.
[sandbox]
region = "eu" # us (default) or eu
templates = ["ondrejs-project/herdr-agents", "claude", "base"]
# A `/` in the name means the table header must be QUOTED:
[templates."ondrejs-project/herdr-agents".env]
ANTHROPIC_API_KEY = "sk-ant-…"
[fleet.agents]
"ondrejs-project/herdr-agents" = "claude --dangerously-skip-permissions"Unquoted, TOML reads the / as part of a bare key, the table never matches, and
the box boots with no credential — the agent then opens on its sign-in screen,
which a fleet member cannot answer for itself.
Two things e2b template list will not tell you:
- Names are per region. The same project name can exist in both regions
owning different templates, so a
templateslist is not portable — changingregionmeans revisiting it. - In the EU the listing omits public templates, returning only your
project's own. A public name missing there is not evidence it is missing:
claude,codex,opencode,amp,droidandbaseall resolve in the EU regardless.
Get the project half wrong and E2B names the project you actually belong to:
400: namespace 'e2b' must match your team 'ondrejs-project'
A member of a fleet is named after the template's last segment, so
ondrejs-project/herdr-agents gives the branch e2b/<slug>-herdr-agents-<rand4>
— the project half is shared by every member, so it only costs a sidebar row its
readability. Two roster entries that would claim the same name are refused.
open [-t NAME] boot or reconnect this checkout's box, then attach
up [-t NAME] same, but the box boots behind you
connect [<id>] attach to a box that already exists
exec <cmd> run one command inside the box, print its output
run --task TEXT headless: boot, hand the task to the box's agent, wait,
pull the result, [--push], tear down; exit 0 only if all landed
sync upload this checkout into the box local → box
pull [--force] download the box's project dir back box → local
pause · resume freeze / thaw — state kept, billing clock stopped
kill destroy the box; checkout and branch stay
status · list this checkout's box · every tracked box
url · logs forwarded URL · follow the provisioning log
wait · doctor block until ready · check node, CLI, credentials, state
dash [toggle] the live board of every tracked box
--json on list/status/wait/run, --timeout-ms N on exec/wait/run,
-b KEY to act on another box. status/list show the last known state; open
reconciles with E2B and reprovisions a box that idle-timed-out.
open and fleet need a pane and a human in it; exec runs one command. run
is the verb with neither: it boots this checkout's box (or re-syncs the tracked
one so it sees the current tree), writes the task into the box as a file, starts
the template's agent in its non-interactive mode and waits for it to exit,
pulls what changed back into this checkout, and pauses the box, so e2b-box open can put you back in the run nobody watched (--kill destroys it instead,
--keep leaves it running). Exit 0 means every step landed; anything else is 1,
with the step that failed in status.
e2b-box run -t claude --task 'fix the login redirect loop'
e2b-box run -t codex --task GOAL.md --push --json # a file: read it; commit + push this branch
cat GOAL.md | e2b-box run -t amp --task - --timeout-ms 3600000 # `-`: stdin
e2b-box run -t claude --task GOAL.md --dry-run # the plan, nothing created--task takes the text itself, the path of a file to read, or - for stdin.
The full contract, the box lifecycle and a GitHub Actions example live in
docs/headless-run.md.
- Templates: every shipped agent template (
claude,codex,opencode,amp,grok,droid,muse,prime) has a verified headless command;base(the default) and your own templates are refused by name until[run.agents]maps them, seeconfig.example.toml. --pushcommits everything the pull left different on the local branch and pushes it to--remote(origin). The box holds a baseline and no history (ADR 0012), so the branch with the real history is the one on this machine. A tree that was dirty before the run is part of the snapshot the agent worked from, and so part of the commit;runsays so.- Clobber guard is
pull's: a file the agent changed that also carries uncommitted local edits aborts the pull and the run fails aspull-failed; the box is never killed then, even under--kill, so nothing is lost.--forceoverrides. --jsonprints one object:{ok, key, sandboxId, template, status, agent: {ok, exitCode, stdout, stderr, error}, pull, push, box, elapsedMs, error}.statusisdone, or the step that decided otherwise:boot-failed,task-failed,agent-unmeasured(never ran to a verdict: unreachable, or killed at--timeout-ms),agent-failed(exited non-zero),pull-failed,push-failed,teardown-failed.boxis what the box is now:paused,killedorrunning.
e2b-fleet # two screens: slug + task, then the roster table
e2b-fleet login-fix --agents claude,codex # …or say it outright
e2b-fleet login-fix --all --task "fix the login redirect loop"
e2b-fleet login-fix -t 'codex*3@ultra' -t grok@xhigh # three codex at ultra, one grok at xhigh
e2b-fleet login-fix --agents claude -n # --dry-run: print the plan, create nothing
e2b-fleet kill login-fix [--prune-branches] # boxes and worktrees gone, branches keptPer member and all at once: a worktree on a fresh branch
e2b/<slug>-<template>-<rand4> off your current HEAD, opened as an ordinary
herdr workspace, with its own box booted from its own template, its agent started
inside it, and the task handed over the moment that agent settles.
The roster screen is a table, one row per template and three cells: in or out, the effort, how many of it.
Roster for fleet login-fix
template effort count
▸ [✓] codex ultra 3 same effort [✓]
[✓] opencode default 1 (pick in opencode's UI)
[ ] claude high 1
[✓] grok each own 2 same effort [ ]
├ #1 xhigh
└ #2 high
[ ] muse high 1
6 members
space tick ↑/↓ move →/tab column +/- count s per-instance effort enter launch (on an effort cell: its list) q abort
+/- set how many members a template gets; they share the row's effort until
s unfolds one row per instance, each with its own cell. s also ticks the
template and grows it to two if it had one, and lands on instance #1 with its cell
focused, so one key gets you to the first thing to change; s again folds. The
model column works the same way, and s gives each instance its own model too, so
one fleet can race a template across models. From the command line the same roster
is a member spec per -t (or comma-separated in --agents):
NAME[:MODEL][*N][@EFFORT], so codex*3@ultra is three codex members at ultra,
claude:claude-opus-5*2 two claudes on Opus 5, and a repeated -t codex is two of
them. Instances are numbered in their labels and
branches (login-fix-codex, login-fix-codex-2). An effort travels to the member
as e2b-box open -t codex --reasoning ultra and overrides the template's pin for
that box only.
- A terminal is what asks for the board. With a tty the pane you launched
from becomes a live board of the roster while it provisions (the per-member
report goes to
$STATE_DIR/fleets/<slug>.log);--no-dashboardkeeps the report instead. Without a tty — an agent, a script, CI — members provision in the foreground and the exit code is 0 only if every one came up.e2b-box fleet …is the same grammar for a script to call. - Members start clean. A member is a fresh worktree off the base ref, so
uncommitted work where you launched from is not carried in. The one place
fleetbehaves unlikeopen. - Nothing is rolled back. A failed member keeps its branch and its worktree.
- Members are ordinary worktrees and boxes —
sync,pull,pause,resume,killall work, and removing one's worktree kills its box. - The fleet is its branch prefix. Nothing is stored anywhere:
git branch --list 'e2b/login-fix-*'is the member list, and it is whatkillglobs.--prune-branchesdeletes the branches too, refusing any whose commits are neither merged nor pushed unless you--force.
[fleet] sets base, prefix and default_roster; [fleet.agents] maps a
template to the command that starts its agent.
Save counts, models and reasoning in a JSON fleet specification:
e2b-box fleet login-fix --file compare.json --task "Fix the login loop".
Put that file in presets/ beside config.toml to use --preset compare or select
it with p / Shift+P in the roster picker. fleet presets lists names, and
--dry-run validates and previews a generated file without creating anything.
Three agents worked the same task; now decide which one did it. One held-out check per member, and the verdict is its exit code — no rubric, no LLM judge (ADR-0004).
e2b-bench login-fix --grade 'npm test' # run the check in every member, print the board
e2b-bench login-fix # re-read a run already graded
e2b-bench # list the graded runs on diskbench 'login-fix'
check: npm test
MEMBER TEMPLATE VERDICT TIME DETAIL
login-fix-claude claude ✓ pass 1m04s
login-fix-codex codex ✗ fail 52s exit 1 2 failing
login-fix-grok grok ! error 3s sandbox is gone
1/3 passed · 1 never measured (box unreachable)
error is not fail — an unreachable box, or one that outlived
--timeout-ms (15m default), never produced a measurement, and folding that into
"failed" would blame an agent for a dead sandbox. The exit code is 0 only when
every member was measured and passed, so CI can gate on it. Verdicts persist in
$STATE_DIR/bench/<slug>/ and outlive the fleet
(ADR-0005). Needs the Rust
toolchain: (cd tui && cargo build --release), which ./install.sh does
whenever cargo is on PATH.
A live TUI of every tracked box. Run e2b-dash, open the dashboard pane, or
press prefix+shift+d (bound in Install).
Run e2b-popup (or e2b-box popup) to show the same dashboard in a centered
overlay, at 90% width and 85% height, with the current panes visible behind it.
It uses the same dashboard binary and controls. q or Esc from the main board
closes the popup and returns to your panes.
Enter (or o) in the popup opens the selected box in a regular pane and closes
the popup, because the overlay's terminal is discarded when it closes. By default
that pane is a split below the pane the popup floats over; [dashboard].popup_open
moves it: "below" (default), "right", "above", "left" (a split on that side
of the pane) or "tab" (a new tab). Herdr itself splits right or down only, so
above and left split and then swap the new pane with the one you were in.
Both views are independently keybindable in Herdr settings: e2b-dash (pane
view) uses the dashboard action, and e2b-popup (overlay view) uses popup.
You can keep both shortcuts in your Herdr config.toml, for example:
[[keys.command]]
key = "prefix+shift+d"
command = "herdr plugin action invoke dashboard --plugin e2b-dev.herdr-e2b"
[[keys.command]]
key = "prefix+ctrl+d"
type = "shell"
command = 'exec "$HOME/.local/bin/e2b-popup"'The popup border shows herdr-e2b-sandbox (the GitHub glyph uses a Nerd Font),
leaving more room for the count and region in the dashboard header. Herdr builds
that supply HERDR_POPUP_VERSION show the version on the top-right border; older
builds keep it in the dashboard header. The shortcut
uses the plugin popup entrypoint so Herdr can display that title. The first frame
reuses the last resolved theme and region while fresh settings load in the
background. Box records are read fresh, and config commands are never cached.
Choose unused keys or replace existing bindings; these are separate actions,
so assigning one does not change the other. Existing dashboard-toggle
bindings continue to use the pane view.
↑/↓ move · ↵/o open · w worktree · s sync · p pull · z pause/resume
x kill · c copy id · C config paths · r refresh · T theme · q quit
Press C (Shift+C) to choose a config path, then Enter to open it with the
shell's open on macOS (xdg-open on Linux), or c to copy its path:
config.toml for templates and manually configured API keys, auth.toml for
discovered credential sources, or connections/ for saved agent connections.
The picker uses the active config directory and works with no boxes. Esc
closes it. Paths can be copied before the files exist.
Set [dashboard].config_opener to customize how Enter opens a path. It runs in
your interactive shell with the Herdr config folder as both the working directory
and $1, and the selected path as $2. The folder resolves to
$XDG_CONFIG_HOME/herdr, or $HOME/.config/herdr when XDG is unset. For example, to open a new editor window:
[dashboard]
config_opener = 'code --new-window "$1" "$2"'E2B_DASH_CONFIG_OPENER overrides this setting. With neither set, the picker
uses your shell's open on macOS or xdg-open on Linux.
The header names the region a new box would land on (us / eu, resolved the
same way every other verb resolves it, re-asked while the board is open) with the
current theme right-aligned beside it. A ⚠ after the region means the list is not
all one cluster — those boxes can't be reached from here.
Columns are NAME · BRANCH · TEMPLATE · SANDBOX · FILES · STATUS. TEMPLATE is
the template the box actually booted; base ⚠ in amber means the requested one was
not built on this cluster and base booted instead. BRANCH is read live from the
worktree's HEAD and is dropped on a pane too narrow for it. STATUS comes last
and carries the provisioning step when there is one to carry
(◐ provisioning · uploading 210/540 files).
sync/pull/kill confirm first and name the exact worktree. w jumps to the
row's local worktree, focusing that herdr workspace if it is already open. T
cycles terminal · solarized-light · tokyo-night · dracula · nord · gruvbox;
[dashboard].theme sets a default. install.sh builds it with Rust when you
have it, otherwise downloads the published binary — offline or on an unsupported
arch it skips, because the dashboard is optional.
open, fleet, sync, pull, status, pause, resume, kill and
dashboard are registered as herdr actions, acting on the focused pane's
checkout:
herdr plugin action invoke sync --plugin e2b-dev.herdr-e2bopen and fleet hand off to a pane — an action has no terminal of its own, so
it can't host a shell or draw a chooser. One-shot verbs print to
herdr plugin log list --plugin e2b-dev.herdr-e2b.
File selection follows git: git ls-files --cached --others --exclude-standard —
tracked files including uncommitted edits, plus untracked files, honoring
.gitignore. Build output, caches and node_modules never go up. .git is
skipped and the box runs git init -b <branch>; [upload].ignore is an extra
filter on top (it keeps .env out even if tracked) and the only filter for
non-git folders. Symlinks are skipped.
Copy config/config.example.toml to
~/.config/herdr/plugins/config/e2b-dev.herdr-e2b/config.toml. Every key is optional and
that file documents all of them — region, template, branch rules, timeout,
auto-pause, per-template env, fleet roster, agents and seeds.
- Sync is on demand.
syncpushes up,pullbrings down — writing only files that differ, never deleting local-only files, prompting (or aborting when headless) on a dirty tree unless--force. Review withgit diff. - One box per checkout, keyed by the folder's absolute path — no git needed; a plain folder works too.
- Removing a worktree kills its box. Cost control, intentional.
- Boxes pause at the idle timeout (
[sandbox].timeout_ms, default 1h — the free-tier cap): a full memory snapshot, so billing stops andopenputs you back with everything still running. Works on the free tier.[sandbox].auto_pause = falserestores kill-at-timeout for throwaway boxes.
Apache-2.0. See LICENSE.