Teardown: collie
AltanS/collie (TypeScript/bun, ~1,170 stars) is a mobile web UI for monitoring and replying to your agents over Tailscale. It is the most productionized plugin in the marketplace and the reference for the thin-launcher pattern and ADR-driven docs. Source mirror: _repos/AltanS-collie/.
The thin-launcher manifest
Every action runs the same shim:
id = "herdr.collie"
[[actions]]
id = "start"
title = "Start Collie"
contexts = ["workspace"]
command = ["bash", "scripts/collie-ctl.sh", "start"]
[[actions]]
id = "update"
title = "Update plugin"
command = ["bash", "scripts/collie-ctl.sh", "update"]
The shim does nothing clever: it resolves bun, builds bin/collie if the checkout has none, and execs that binary with the same argv. The CLI in cli/ is the one implementation of every verb.
Why route through a shim instead of pointing actions at the binary? Because of what herdr does (and does not do) with command strings at install time.
Frozen command strings (ADR 0006)
On herdr < 0.8.0, a managed install invokes the action set cached at install time. Change the bytes of a command string in a later release and every old install still tries to run the old string. Collie’s answer, recorded in ADR 0006:
- The command strings are frozen, byte for byte:
bash scripts/collie-ctl.sh <verb>must keep resolving forever. - Behavior changes happen behind the shim, in the script or the binary.
- Even README recipes spell the verbs the frozen way, so docs and installs agree.
[[build]] is the one exception allowed to be different: it is the step that produces the binary, so it cannot invoke it.
Gotcha: this applies to you even if your plugin is trivial. Renaming a binary or script path in
herdr-plugin.tomlstrands every install that cached the old command string. Treat manifest commands as a public API. More in version-compat-doctrine.md.
Process model: outlive herdr
The real bridge runs as a systemd --user service so it survives herdr restarts. Herdr is the launcher and control plane; the long-running process is deliberately outside it. Consequences:
start/stop/restart/uninstallactions manage the service, not a child process.- An
updateaction pulls, rebuilds, and restarts in one step (link-mode plugins are their on-disk checkout, and there is no nativeplugin update). - A separate
update-majoraction exists because crossing a major version needs explicit consent, and a plugin action is the only place that consent can be expressed when the user is on their phone with no TTY.
Secrets and config
Config surfaces as *.toml.example files in the repo (keys.toml.example, cache-rules.toml.example, …); users copy them into the plugin’s config dir. Herdr injects HERDR_SOCKET_PATH and HERDR_PLUGIN_CONFIG_DIR into invocations and nothing else, and the shim and binary both rely on exactly that contract.
Docs culture
The repo ships ARCHITECTURE.md with numbered ADRs, AGENTS.md, CLAUDE.md, CHANGELOG.md, and a HERDR_API.md capturing the herdr surface it depends on. This is the norm among top plugins (file-viewer and zoetrope do a lighter version of the same): AI-collaboration docs are table stakes because a large share of contributors are agents.
What to copy vs skip
| Copy | Skip (for now) |
|---|---|
| Thin shim with frozen verbs | systemd service (only if your plugin has a daemon) |
| ADRs for compatibility decisions | Full example-file config zoo |
update action for link-mode users | Multi-CLI verb surface, unless your app needs it |
| HERDR_API.md dependency ledger |