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
| Variable | Set for | Contents |
|---|---|---|
HERDR_ENV | all runtime commands | "1", a marker that you run inside herdr |
HERDR_BIN_PATH | all runtime commands | Path to the running herdr binary; use this for CLI callbacks |
HERDR_SOCKET_PATH | all runtime commands | Unix socket path (named pipe on Windows) |
HERDR_PLUGIN_ID | all runtime commands | Your plugin id |
HERDR_PLUGIN_ROOT | all runtime commands | The installed or linked plugin directory |
HERDR_PLUGIN_CONFIG_DIR | all runtime commands | User-editable config directory (herdr creates it) |
HERDR_PLUGIN_STATE_DIR | all runtime commands | Durable runtime state directory (herdr creates it) |
HERDR_PLUGIN_CONTEXT_JSON | all runtime commands | Invocation context (see the event model) |
HERDR_PLUGIN_EVENT | event + startup hooks | Event name; "startup" for startup hooks |
HERDR_PLUGIN_EVENT_JSON | event hooks | Serialized event envelope |
HERDR_PLUGIN_ACTION_ID | actions | The invoked action id |
HERDR_PLUGIN_ENTRYPOINT_ID | pane commands | The [[panes]] id being opened |
HERDR_WORKSPACE_ID, HERDR_TAB_ID, HERDR_PANE_ID | when available | Convenience duplicates of context fields |
HERDR_PLUGIN_CLICKED_URL, HERDR_PLUGIN_LINK_HANDLER_ID | link handler actions | Clicked 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
| Directory | Managed by | Put here |
|---|---|---|
HERDR_PLUGIN_ROOT | herdr (for installs: it is the git checkout) | Nothing at runtime |
HERDR_PLUGIN_CONFIG_DIR | you (herdr creates and seeds it) | User-editable config: TOML files, .env with secrets |
HERDR_PLUGIN_STATE_DIR | you (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 inCONFIG_DIR, state inSTATE_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.