Common Failures
An error-catalog for plugin development: what each error means, why it happens, and how to fix or design around it. Debug commands live in how-to/debug-plugins.md; the runtime internals behind most of these are in deep-dives/core-runtime-internals.md.
Error reference
| Error / symptom | Cause | Resolution |
|---|---|---|
plugin_requires_newer_herdr | The plugin’s min_herdr_version exceeds the running herdr. The check re-runs on every dispatch, not just install. | Lower the floor to the oldest herdr that works. Gate newer features at runtime and declare newer events freely (unknown events are only install-preview warnings). See version-compat-doctrine.md. |
events_lost | Herdr delivers events in full bursts; a reader (your handler) too slow to drain gets dropped deliveries (0.9.2+). | Keep handlers cheap: exit early on irrelevant events, avoid network I/O in hooks, debounce instead of reacting to every event. Subscribe before snapshotting so a missed burst is recoverable from api snapshot. |
| Install/build aborts after a manifest change | Herdr shows an install preview, then runs [[build]]; a manifest edited between preview and build aborts the install. | Redo the install with the final manifest in place. Never treat manifest edits as safe mid-flight changes. |
ui_busy (“a popup pane is already open”) | Plugin tried to open a popup pane while another modal is up: Settings, copy mode, or an existing popup. Popups are session-modal singletons. | Catch it and retry on the next event, or fall back to overlay/split placement when content must always open. |
| Same-id install silently replaces another plugin | Installed plugins are a map keyed by plugin_id; installing an existing id overwrites the entry. Two same-id plugins cannot coexist on one machine. | Pick a unique id before shipping: username-prefix (peteretelej.<name>) and check the marketplace index first (see ship-to-marketplace.md). Symptoms of a collision: your action list changes, an unfamiliar plugin root shows in plugin list. |
Plugin action gets no selected_text | Selection is client presentation state; the server-side context always carries selected_text: None. Tracked as issue #3380. | Design actions that do not need the selection, or have them prompt for input. Link handlers get clicked_url instead, which does reach plugins. |
Missing tab.focused events with config keybinds | Tab focus switching via user-configured keybinds does not emit tab.focused on some paths. Tracked as issue #3801. | Do not rely on tab.focused alone for correctness. Subscribe to multiple events (pane-topic-sync pattern) and reconcile; the idempotent-reconcile design tolerates missed triggers. |
agent_not_idle on agent read | Alternate-screen agents (Claude Code, OpenCode) page history via their mouse-scroll UI, which herdr only drives while the agent is idle. While working/blocked/unknown the read is refused. | Use --source visible (passive), wait for idle and retry, or ask the agent to write state to a file. See read-agent-state.md. |
| Prompt retry duplicates the message | agent prompt --wait can stall (agent_prompt_stalled after 5s without activity) or reject with agent_blocked; blind retries send the prompt twice. | Read the agent first (agent get) before retrying; treat agent_blocked as terminal until unblocked; observe the completion_seq/response rather than assuming failure. |
| Handler runs stale code while linked | plugin link skips [[build]]; herdr launches whatever binary is at $HERDR_PLUGIN_ROOT/target/release/. | cargo build --release after edits. Confirm the manifest command path and the binary timestamp. |
No such file or directory on the binary path | Fresh clone linked without ever building. | Build once before linking, or add a dev note in the README. |
duplicate_plugin_action_id at load | Two [[actions]] (or panes/link handlers) share an id, even across different platforms gating. | Make ids unique within the plugin; platform variants need distinct ids (file-viewer uses -windows suffixes). |
invalid_plugin_min_herdr_version | min_herdr_version missing or not a bare semver. | Set an explicit semver value like "0.7.0". |
| Pane freezes at a stale topic; plugin never renames it | The pane’s live label stopped matching the plugin’s stored label (single-label state file + concurrent runs), so the plugin classified it as manually renamed. | Store a history of written labels, not just the latest; include the desired check in ownership so self-healing works. See teardown-pane-topic-sync.md. |
| Tabs never get renamed; all tabs look “manually renamed” | Comparing tab labels against tab.number (persistent id) instead of the switch-position string (the virgin default), once positions drift. | Treat the virgin tab label as its 1-based switch position within the workspace. |
| Popup content cut off / below the fold | Default popup is about 24 rows; your UI needs more. | Set explicit width/height on the [[panes]] entry; herdr clamps to the terminal. |
| Plugin talks to the wrong server in hand tests | Plugin panes and manual runs inherit HERDR_SOCKET_PATH from the launching herdr. | Clear it for manual runs against a scratch server: env -u HERDR_SOCKET_PATH herdr --session scratch. |
Design rules that prevent most of these
- Reconcile, do not react. An idempotent handler that recomputes full state survives missed events, bursts, and overlapping runs.
- Treat the manifest as a public API. Changing command bytes breaks installs that cached them (herdr < 0.8.0).
- Read defensively. Payloads gain fields;
#[serde(default)]everywhere. - Fail silent, log everything.
stderrfrom your handler is the user’s debugging trail viaplugin log list. - Never store state in
HERDR_PLUGIN_ROOT. It is a managed checkout; reinstalling wipes it. UseHERDR_PLUGIN_STATE_DIR.