Skip to content

Using nexus from herdr

herdr is the terminal workspace manager nexus integrates with. The plugin turns every sandbox into something you can see and act on without leaving herdr: a listing overlay, a guest shell in a pane, and lifecycle actions bound to the focused workspace.

Install the plugin built

One-command install (Linux x86-64)

The plugin is self-bootstrapping. One command downloads, verifies, and installs everything:

sh
herdr plugin install IniZio/nexus/plugins/herdr

build.sh (the plugin's build hook) reads plugins/herdr/nexus-version for a pinned release tag, downloads nexus-linux-amd64 and SHA256SUMS from GitHub Releases, verifies the checksum, installs the binary to ~/.local/bin/nexus, ABI-probes it, then runs nexus herdr install-default-shell --write-config to hard-link the guest shell and wire config.toml automatically.

Manual fallback. If install-default-shell --write-config is skipped or fails the herdr config check step, it prints the line to paste:

[terminal]
default_shell = ~/.local/bin/nexus-guest-shell

Paste it into ~/.config/herdr/config.toml and run herdr server reload-config.

Confirm the plugin loaded:

sh
herdr plugin list

Platform matrix

PlatformStatus
Linux x86-64supportednexus binary downloaded and installed by build.sh
macOS arm64supportednexus-client downloaded from the pinned release by build.sh
macOS amd64supportednexus-client downloaded from the pinned release by build.sh
Linux arm64no released binary — build from source (see below)

macOS users run the same install command as Linux:

sh
herdr plugin install IniZio/nexus/plugins/herdr

build.sh detects the platform, downloads the matching nexus-client binary from the pinned release, verifies its checksum, and installs it to ~/.local/bin/nexus-client. No Go toolchain required.

For Linux arm64, build.sh exits with a clear message. Build and wire manually:

sh
git clone https://github.com/IniZio/nexus
cd nexus && go build -o ~/.local/bin/nexus ./cmd/nexus
nexus herdr install-default-shell --write-config

Then wire the plugin from a local clone (two steps — herdr plugin link does not run the build hook, so run build.sh first):

sh
cd /path/to/nexus/plugins/herdr
NEXUS_LOCAL=1 sh build.sh
herdr plugin link /path/to/nexus/plugins/herdr

Local-dev path

If nexus is already on PATH and you want to skip the download entirely (e.g. while iterating on the binary itself):

sh
cd /path/to/nexus/plugins/herdr
NEXUS_LOCAL=1 sh build.sh
herdr plugin link /path/to/nexus/plugins/herdr

herdr plugin install only accepts OWNER/REPO (GitHub) source; it cannot install from a local path. herdr plugin link registers the manifest but does not run the [[build]] hook. Run build.sh manually before or after linking; NEXUS_LOCAL=1 skips the download and uses the binary already on PATH.

build.sh writes the shim next to the plugin files (plugins/herdr/nexus-shim.sh in a checkout install), and that shim is what every live hook execs. Override INSTALL_DIR for a throwaway run only together with NEXUS_SHIM_DIR=<dir>; without it build.sh refuses rather than repoint the live shim at a temporary binary.

Rebuild the binary, not just the plugin

The shim records the absolute path of the installed binary. If you install a new binary to a different location, re-run the install so the shim is rewritten. A stale shim is the most common cause of "herdr shows old behaviour".

Sandbox lifecycle in herdr built

Create: transactional open

When you run nexus: create a sandbox (or open a worktree pane), nexus creates three things in sequence: the sandbox, the herdr workspace, and the binding that links them. If any step fails before the binding is written, the incomplete pieces are rolled back — you do not end up with orphaned workspaces. A sandbox that already exists and is running is never destroyed by a failed pane-open.

Teardown: one idempotent path

Whether teardown is triggered by the nexus: remove this sandbox and close the space action, nexus herdr remove, a nexus rm cascade, or the real-time pane-close reap described below, it follows the same path: remove the sandbox, close the herdr workspace, delete the binding. If the workspace close does not succeed, the binding is retained rather than deleted, so nexus herdr prune --apply can retry it on the next pass — the only copy of the record is never lost.

Real-time reap on pane close

Closing the last pane of a worktree sandbox tears it down automatically. herdr sends a trappable SIGHUP to the process group; nexus catches it and runs the teardown path above. This is best-effort. If the signal arrives while nexus is in a non-interruptible state, or if herdr is not available, the teardown is skipped and the binding remains. nexus herdr prune --apply is the reliable backstop — run it after a session to confirm nothing leaked.

prune --apply: 4-case reconciler

nexus herdr prune (dry-run by default) inspects every binding and classifies it:

Binding stateAction
sandbox gone + workspace gonedelete binding
sandbox gone + workspace liveclose workspace, delete binding
sandbox live + workspace gonekeep binding, clear stale workspace ID
both livenoop

It also sweeps for nexus:-labelled orphan workspaces that have no binding at all, and closes them.

sh
nexus herdr prune            # dry-run: report what would change
nexus herdr prune --apply    # apply: close workspaces and delete stale bindings

Global prune --apply is a manual verb. A workspace whose tab you merely closed is absent from herdr workspace list, so a global apply reaps its sandbox too. The worktree.removed hook therefore never runs it; it runs nexus herdr prune --apply --workspace <id>, which takes the workspace id from the event payload and reconciles only that one binding (the named workspace is taken as gone; no orphan or bindingless sweep runs). Run the dry form first to see what a global apply would take.

Known residue (D-SHL-27)

The first worktree you open for a given repo opens a plain host shell — not a supervised guest pane. Every subsequent worktree for that repo auto-creates a supervised sandbox as expected. This is a known limitation left in place by decision D-SHL-27. The first worktree's sandbox, if one is left orphaned, is reclaimed by:

sh
nexus herdr prune --apply

What the overlay shows built

Open it with the nexus: list sandboxes action. herdr has no built-in action menu key, so actions are invoked through whatever you have bound to a palette — with the jt.command-palette plugin that is prefix+p — or from a terminal:

sh
herdr plugin action invoke workspaces --plugin nexus

The overlay lists every sandbox, however it was created:

WORKSPACE     STATE    AGENT        MOUNTS              SPACE  ID
demo/api      running  -            /work               bound  sb-06G1…
demo/agent-1  running  claude-code  /work,nm→/work/nm   -      sb-06G1…
ColumnWhat it answers
WORKSPACEthe project/name handle you pass to every other command
STATErunning, paused, stopped, created, error
AGENTwhich agent profile it was created for, or - for a plain sandbox
MOUNTSguest paths of live host mounts, then volume→path for named volumes
SPACEbound if a herdr workspace is attached, - if not
IDthe sandbox ULID, for commands that want an unambiguous ref

MOUNTS is the column that usually decides what to open: a sandbox with no mounts gives you a guest shell in /root, while one with a live mount drops you straight into the mounted directory.

The same information is available in a terminal with nexus ps.

Actions built

These appear in herdr's action list for the focused workspace:

ActionEffect
nexus: list sandboxesopen the listing overlay described above
nexus: create a sandboxprompt for image, project and name, then create, boot and open a space
nexus: attach to a workspacereattach to an existing guest session
nexus: create sandbox space (from local Containerfile)build, boot, and open a space in one step
nexus: open guest paneanother guest shell in the current space
nexus: pause this sandboxpause the bound sandbox — frees CPU, keeps memory state
nexus: resume this sandboxresume it
nexus: remove this sandbox and close the spaceremove the sandbox, close the workspace, drop the binding
nexus: workspace logsnot built prints a not-implemented notice
nexus: doctorsubstrate and plugin diagnostics

Every action resolves the sandbox from the focused herdr workspace, so none of them asks you to type a handle.

Sandboxes created outside herdr built

A sandbox made in a terminal is a first-class herdr citizen:

sh
nexus create demo/api --image ghcr.io/inizio/nexus-base:latest

It appears in the overlay immediately, because the listing is unfiltered. The first time you run a herdr action against it, nexus adopts it — creating the binding that the action needs and telling you so:

nexus: adopted sandbox demo/api into herdr as nexus:demo/api

Adoption is deliberately lazy. Binding at creation time would mint a herdr workspace for every throwaway sandbox — including the ones nexus run creates and deletes seconds later — and would make sandbox creation depend on herdr being installed. Doing it on first use costs nothing until you actually ask herdr to act.

Adoption does not create a herdr workspace. Pause, resume and remove need only the sandbox handle; only opening a pane needs a workspace, and that one is created at the moment it is needed. Otherwise pausing a sandbox would leave an empty workspace behind every time.

Run nexus herdr list to see the current bindings.

A working session

sh
# create a sandbox with your repo mounted live
nexus create demo/api --image ghcr.io/inizio/nexus-base:latest --mount "$PWD:/work"

# see it
nexus ps

Then, in herdr: run the nexus: list sandboxes action, find demo/api, and use nexus: open guest pane. The pane opens a login shell already in /work, because the shell's working directory is derived from the sandbox's first live mount.

When you are done, nexus: remove this sandbox and close the space tears down the sandbox, the workspace, and the binding together. Verify nothing leaked:

sh
nexus reap

Update the plugin built

To upgrade nexus to the version pinned in the plugin's nexus-version file, re-run the same install command:

sh
herdr plugin install IniZio/nexus/plugins/herdr

The build hook (build.sh) compares the installed binary against the pin. It upgrades an older release build, and leaves a -dev build or a binary newer than the pin in place. To check whether your installed version matches the pin, run:

sh
nexus herdr version-check

This prints one of: version ok: <ver> (matches pin), a kept-newer or dev message, or version skew: installed <old> < pinned <new> with the exact update command on stderr. The same check runs at herdr startup and in nexus herdr doctor.

Restart herdr after a plugin upgrade

After running herdr plugin install …, the new binary is installed but the running client daemon (nexus-client herdr local-agent-startup) continues using the old code. To activate the new version, restart herdr:

sh
herdr server stop
# then relaunch herdr normally

Until herdr is restarted, port forwarding continues with the old daemon behaviour.

Troubleshooting

An action says a sandbox does not exist. The binding outlived the sandbox — something removed it outside herdr. nexus herdr list shows the stale entry; removing through herdr again clears it.

The overlay is empty but you know a sandbox exists. The overlay reads the same store as nexus ps. If ps also shows nothing, check that both are running as the same user: state lives under the user's own state directory.

herdr shows behaviour you already fixed. Rebuild nexus and re-run the plugin install, per the warning above.

stop says the sandbox is still running. That is the honest answer, not a bug in the report: the detached supervisor did not finish within its timeout, so the record still reads running. Wait a moment and check nexus ps; if the state does not settle, nexus reap will show whether anything leaked.

Ports are not forwarded even though sandboxes are running. The client daemon forwards ports for the sandbox bound to the currently focused herdr workspace on the host session. When no workspace is focused, or the focused workspace has no bound sandbox, no forwards are active — the daemon logs portfwd focus: focused workspace has no sandbox; forwarding nothing and cancels any previously applied forwards within one reconcile tick. Look for that line in ~/.local/state/nexus/portfwd-client/agent.log. Switch focus to the workspace that owns the sandbox, or run nexus herdr list on the host to confirm a binding exists.

Sole-interactive-client rule

herdr 0.9.0 maintains one session-wide focus state shared across all connected clients. The nexus-client daemon reads the focus from the host session. Focus-scoped forwarding is accurate only when nexus-client is the sole interactive client on that herdr session. When a host TUI and a remote client are both attached, the host session's most recent workspace focus is the one the daemon reads; the remote client has no independent focus channel.

Mac remote-client focus on herdr 0.9.0

herdr 0.9.0 does not reflect Mac remote-client focus changes in session.snapshot, workspace.list, events.subscribe, or any plugin hook. The host-side focus watcher works around this by tailing the session server log (~/.config/herdr/sessions/<session>/herdr-server.log, or ~/.config/herdr/herdr-server.log for the default session) and parsing lines with event="workspace.focus" and outcome="ok". This log-tail source is polled every 250ms; it is the authoritative source for remote-client focus until the upstream protocol exposes a per-client focus surface.