The runtime environment

Herdr injects a fixed set of environment variables into every command it launches for a plugin: build steps excepted. Your binary reads these instead of hard-coding paths or discovering the socket itself.

Environment variables

VariableSet forContents
HERDR_ENVall runtime commands"1", a marker that you run inside herdr
HERDR_BIN_PATHall runtime commandsPath to the running herdr binary; use this for CLI callbacks
HERDR_SOCKET_PATHall runtime commandsUnix socket path (named pipe on Windows)
HERDR_PLUGIN_IDall runtime commandsYour plugin id
HERDR_PLUGIN_ROOTall runtime commandsThe installed or linked plugin directory
HERDR_PLUGIN_CONFIG_DIRall runtime commandsUser-editable config directory (herdr creates it)
HERDR_PLUGIN_STATE_DIRall runtime commandsDurable runtime state directory (herdr creates it)
HERDR_PLUGIN_CONTEXT_JSONall runtime commandsInvocation context (see the event model)
HERDR_PLUGIN_EVENTevent + startup hooksEvent name; "startup" for startup hooks
HERDR_PLUGIN_EVENT_JSONevent hooksSerialized event envelope
HERDR_PLUGIN_ACTION_IDactionsThe invoked action id
HERDR_PLUGIN_ENTRYPOINT_IDpane commandsThe [[panes]] id being opened
HERDR_WORKSPACE_ID, HERDR_TAB_ID, HERDR_PANE_IDwhen availableConvenience duplicates of context fields
HERDR_PLUGIN_CLICKED_URL, HERDR_PLUGIN_LINK_HANDLER_IDlink handler actionsClicked URL and matched handler

Runtime commands run with the plugin directory as their working directory. Build commands get none of this: no context, no socket env.

Calling back

Prefer HERDR_BIN_PATH over raw socket access. The socket transport is OS-specific (Unix socket vs Windows named pipe); the CLI hides that difference and gives you the full command surface:

let herdr = std::env::var("HERDR_BIN_PATH").unwrap_or_else(|_| "herdr".to_string());
let out = std::process::Command::new(herdr).args(["workspace", "list"]).output()?;

Use the raw socket (HERDR_SOCKET_PATH) only when you need methods the CLI does not expose or want to avoid process-spawn overhead.

The three directories

DirectoryManaged byPut here
HERDR_PLUGIN_ROOTherdr (for installs: it is the git checkout)Nothing at runtime
HERDR_PLUGIN_CONFIG_DIRyou (herdr creates and seeds it)User-editable config: TOML files, .env with secrets
HERDR_PLUGIN_STATE_DIRyou (herdr creates it)Durable runtime state: state JSON, caches, databases

Gotcha: Never store credentials or durable state in HERDR_PLUGIN_ROOT. A GitHub-installed plugin root is a managed source checkout; reinstalling replaces the whole tree and your users’ data with it. Config in CONFIG_DIR, state in STATE_DIR, always.

Herdr creates both user directories and seeds CONFIG_DIR from legacy plugin config locations when present, then never touches the contents again. Your plugin owns the file format and lifecycle. herdr plugin config-dir <id> prints the config directory for setup docs and shell scripts.

There is no herdr-managed storage API in plugin v1. Plugins that need durable state own their files. The ecosystem convention is a single state JSON in HERDR_PLUGIN_STATE_DIR written atomically (temp file + rename) so overlapping event runs cannot corrupt it.

Startup hooks

[[startup]] commands run once for each enabled plugin after herdr restores the session and its API socket is ready, and again when a new server takes over during live handoff. They do not run when a client attaches, on config reload, or when a plugin is linked or enabled.

Treat them as one-shot initialization, not supervised daemons:

fn restore() -> Result<()> {
    let state = std::env::var("HERDR_PLUGIN_STATE_DIR")?;
    let saved = std::fs::read_to_string(Path::new(&state).join("view.json"))?;
    reaply(saved)
}

Herdr starts them asynchronously and records completion in the normal plugin log. A startup failure does not stop the server. Startup hooks receive the standard runtime env plus HERDR_PLUGIN_EVENT=startup, so a binary that shares one entry point for hooks and startup can branch on the event name.

The typical shape: persist a declarative view (what agent rows or metadata you want) under HERDR_PLUGIN_STATE_DIR from an action, then reapply it from the startup hook after every restore or handoff.

Argv-only commands

The manifest’s command arrays are argv, never shell lines. This is a security boundary as much as a convenience: install previews show users exactly which process runs, with no hidden eval. If you need expansion or chaining, start a shell explicitly (sh -c ...) so it is visible in the manifest.