The plugin manifest

herdr-plugin.toml is the contract between herdr and your plugin. It declares package metadata, supported platforms, optional build commands, and every entrypoint herdr can launch. A realistic event + action plugin:

id = "peteretelej.tab-topic"
name = "Tab Topic"
version = "0.1.0"
min_herdr_version = "0.9.0"
description = "Rename agent panes and tabs to their live topic"
platforms = ["linux", "macos"]

[[build]]
command = ["cargo", "build", "--release"]

[[startup]]
command = ["sh", "-c", "exec \"$HERDR_PLUGIN_ROOT/target/release/tab-topic\" restore"]

[[events]]
on = "pane.agent_status_changed"
command = ["sh", "-c", "exec \"$HERDR_PLUGIN_ROOT/target/release/tab-topic\" run --event \"$HERDR_PLUGIN_EVENT\""]

[[actions]]
id = "sync"
title = "Sync pane and tab topics"
contexts = ["workspace"]
command = ["sh", "-c", "exec \"$HERDR_PLUGIN_ROOT/target/release/tab-topic\" sync"]

[[panes]]
id = "dashboard"
title = "Topic dashboard"
placement = "popup"
width = 78
height = 24
command = ["sh", "-c", "exec \"$HERDR_PLUGIN_ROOT/target/release/tab-topic\" dashboard"]

[[link_handlers]]
id = "github-issue"
title = "Open GitHub issue in dashboard"
pattern = "^https://github\\.com/[^/]+/[^/]+/(issues|pull)/[0-9]+$"
action = "sync"

Required and optional fields

FieldRequiredPurpose
idyesGlobally unique plugin id
nameyesDisplay name
versionyesSemver-style version shown in listings
min_herdr_versionyesOldest herdr release that supports every API you use
descriptionnoOne-line description for listings and previews
platformsnoWhere the plugin can run: "linux", "macos", "windows"

Set min_herdr_version to the oldest herdr that supports your event names, manifest fields, and API calls. Herdr refuses to link or install a plugin whose minimum is newer than the running binary. See distribution and versioning for why this should be a floor, not a target.

Id rules

Plugin ids may use ASCII letters, digits, dot, colon, underscore, and hyphen. Action ids, pane ids, and link handler ids are local to the plugin: same charset but no dots. Each id type must be unique inside the plugin; herdr rejects duplicates at link time (duplicate_plugin_action_id). When herdr needs a globally unique name it qualifies local ids as <plugin-id>.<action-id>, e.g. peteretelej.tab-topic.sync.

Gotcha: The marketplace tolerates duplicate plugin ids across repos, but a machine can hold only one plugin per id. Installing a same-id plugin replaces the previous one. Check the marketplace index before naming anything; a username prefix like peteretelej. avoids collisions.

The six sections

SectionRunsKey fields
[[build]]Only during GitHub plugin installcommand, platforms
[[startup]]Once per enabled plugin after session restorecommand, platforms
[[actions]]User trigger (menu, keybinding, CLI invoke, link handler)id, title, contexts, command, description, platforms
[[events]]Every time the named event fireson, command, platforms
[[panes]]Pane open (plugin pane open)id, title, placement, width, height, command, platforms
[[link_handlers]]Ctrl+click on a matching URLid, title, pattern, action, platforms

All sections are optional. A manifest with only [[actions]] is a valid first plugin.

Notes per section:

  • [[build]] commands run after install confirmation and before registration. A failing build aborts the install. They receive no plugin context or socket env, so keep them to plain toolchain invocations. plugin link never runs them; you build your working tree yourself.
  • [[events]] on accepts the hookable event names only (22 as of 0.9.3). See the event model.
  • [[panes]] placement defaults to overlay; use popup for a session-modal singleton and split, tab, or zoomed for ordinary panes. width/height take cell numbers or strings like "80%".
  • [[link_handlers]] pattern is a Rust regular expression matched against the clicked URL; action must name one of your own actions. Handlers are checked in manifest order.
  • Actions become keybindable from herdr config:
[[keys.command]]
key = "prefix+l"
type = "plugin_action"
command = "peteretelej.tab-topic.sync"
description = "sync topics"

Platform gating

Top-level platforms declares where the plugin runs. Any item (build step, hook, action, pane, link handler) can declare its own platforms, which override the top-level list for that item:

platforms = ["linux", "macos", "windows"]

[[build]]
command = ["cargo", "build", "--release"]
platforms = ["linux", "macos"]

A local plugin without top-level platforms links with a warning. GitHub-installed plugins should declare platforms explicitly.

Commands are argv, not shell

command values are argv arrays. Herdr does not run them through a shell, so there is no $VAR expansion, no pipes, no &&:

# No shell expansion happens here:
command = ["node", "dist/apply.js"]

# Start a shell yourself when you need one:
command = ["sh", "-c", "exec \"$HERDR_PLUGIN_ROOT/target/release/tab-topic\" sync"]

The sh -c exec "..." pattern is the ecosystem standard for compiled plugins: it expands $HERDR_PLUGIN_ROOT and exec replaces the shell with your binary so signals behave. Note the manifest also passes env vars through untouched, so binaries can read HERDR_* directly without a shell; the shell only earns its place when you need expansion or chaining.

Manifest location

plugin install accepts owner/repo[/subdir] and finds herdr-plugin.toml at the repo root or in any subdirectory of the default branch. Monorepos ship one manifest per plugin directory; the marketplace lists each valid manifest as a separately installable plugin under one repo card. The lab suite uses the subdir layout: peteretelej/herdr-plugins/<name>/.

Gotcha: Changing herdr-plugin.toml after the install preview aborts the install. The preview lists every build, startup, and hook command so users can review what will run; regenerate the preview after any manifest edit.