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
| Field | Required | Purpose |
|---|---|---|
id | yes | Globally unique plugin id |
name | yes | Display name |
version | yes | Semver-style version shown in listings |
min_herdr_version | yes | Oldest herdr release that supports every API you use |
description | no | One-line description for listings and previews |
platforms | no | Where 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
| Section | Runs | Key fields |
|---|---|---|
[[build]] | Only during GitHub plugin install | command, platforms |
[[startup]] | Once per enabled plugin after session restore | command, platforms |
[[actions]] | User trigger (menu, keybinding, CLI invoke, link handler) | id, title, contexts, command, description, platforms |
[[events]] | Every time the named event fires | on, command, platforms |
[[panes]] | Pane open (plugin pane open) | id, title, placement, width, height, command, platforms |
[[link_handlers]] | Ctrl+click on a matching URL | id, 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 linknever runs them; you build your working tree yourself.[[events]]onaccepts the hookable event names only (22 as of 0.9.3). See the event model.[[panes]]placementdefaults tooverlay; usepopupfor a session-modal singleton andsplit,tab, orzoomedfor ordinary panes.width/heighttake cell numbers or strings like"80%".[[link_handlers]]patternis a Rust regular expression matched against the clicked URL;actionmust 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.tomlafter 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.