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.toml strands 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/uninstall actions manage the service, not a child process.
  • An update action pulls, rebuilds, and restarts in one step (link-mode plugins are their on-disk checkout, and there is no native plugin update).
  • A separate update-major action 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

CopySkip (for now)
Thin shim with frozen verbssystemd service (only if your plugin has a daemon)
ADRs for compatibility decisionsFull example-file config zoo
update action for link-mode usersMulti-CLI verb surface, unless your app needs it
HERDR_API.md dependency ledger