A phone web UI for your Herdr agent herd, served over Tailscale. Open a URL, see which agent is waiting on you, and answer it with your phone's keyboard.
The reply box is an ordinary text field, so your phone's own voice dictation works in it; Collie ships none of its own.
It assumes a Tailscale tailnet — your phone and the host on the same one — and it is single-user: one operator, one tailnet, no multi-tenant auth. If you need shared or public access, Collie isn't built for it. Read the security note either way.
Features
- React Router + Vite — TypeScript, Tailwind, shadcn, and a Bun bridge
- A dashboard ranked by who needs you, not by what changed last
- Push notifications the moment an agent is waiting on you
- Quick actions and slash commands per agent — tap, don't type
- Special-keys pad —
Esc,Ctrl+C, arrows, combinable modifiers - Find in output, and conversation history the terminal can't scroll back to
- Send an image from your camera roll
- Switch between Herdr sessions without touching the host
- Installs to your home screen (PWA) and runs entirely on your own machine — loopback bind, no cloud, no account
- Demo
- Security — read first
- Requirements
- Install
- First run — what you'll see
- Configure · Your own slash commands · Multi-session
- Dark mode / light mode
- Commands
- Manage & update
- Deployment variants · B–E in
DEPLOYMENT.md - Windows (experimental)
- Web Push
- Troubleshooting
- Architecture
- Developing this plugin
A run through the herd from a phone: the dashboard floats the agent that needs you to the top,
you drill into a space's tabs and panes (long-press a pane pill or a tab chip to rename or close it —
and a Claude pane shows the name you gave it with /rename), answer an AskUserQuestion prompt with
a tap, switch between herds, and pick up a push notification the moment an agent is waiting on input.
Collie is remote shell access to your machine, by design. One bridge call types arbitrary keystrokes into a live terminal pane, so anyone who can reach the URL can read every pane (source, secrets, env, agent output) and run any command as your user. No sandbox, no command allow-list (that would defeat the purpose). Treat the URL like a root login.
The sharp edges:
- It acts as you, with your full privileges —
~/.ssh,git push --force,rm -rf,sudo. - Access is device-level, not person-level. Tailscale proves the device, not who's holding it — no password, no session, so an unlocked or stolen phone is an open shell. The idle lock pauses an unattended screen and gates nothing (details: ADR 0007).
- Every uid on the host can reach it. Herdr's socket is a file, so its permissions keep other local users out; Collie's port is TCP, so they're all in. The per-device gate closes the write half of that; reads stay open, so it bounds damage, not disclosure (details: ARCHITECTURE.md §6).
- One bridge fronts every session under your config root by default, sandbox ones included (details: Multi-session).
- Every write is appended to
<state-dir>/audit.log— replies, keys, uploads, pane and tab create/close. A trail is not a gate (details: ARCHITECTURE.md §6). - The defenses: loopback bind only, never
0.0.0.0; exactly one hardened front door —tailscale serveor a conforming reverse proxy, neverfunneland never a bare port; a same-origin gate and a strict CSP, with pane output rendered as React text nodes rather thaninnerHTML. Two settings are yours to switch on, and you should:COLLIE_TRUSTED_USERrejects any tailnet login but yours, andCOLLIE_PUBLIC_HOSTSblocks DNS rebinding (effectively mandatory underCOLLIE_SERVE_MODE=http). Authorising individual devices needs a proxy in front — seeDEPLOYMENT.md.
🚫 Never
tailscale funnelthis — funnel exposes it to the public internet;servekeeps it tailnet-only. There is no scenario where funneling Collie is correct.
Narrow the blast radius with Tailscale ACLs and COLLIE_TRUSTED_USER. Provided as-is, no warranty.
On the host (the tailnet node your agents run on). Need Herdr 0.7.0+ — check with
herdr --version.
| Tool | Why |
|---|---|
| Bun | Runs the bridge and builds the web UI — the only hard dependency. |
| Herdr ≥ 0.7.0 | The herd Collie mirrors; its CLI registers the plugin. |
| Tailscale | Front door for the default variant (tailscale serve); optional if you run Variant C behind your own reverse proxy. Without any front door, the bridge is 127.0.0.1-only. |
| git | Clone, and the update command. |
Soft dependencies: Node.js (the control script uses it to extract your MagicDNS name from
tailscale status --json; without it the banner falls back to the loopback URL) and a service
supervisor — systemd --user on Linux, launchd on macOS (both ship with the OS); a host with
neither falls back to an unsupervised nohup process. You never install JS
deps by hand — the build runs bun install for you; the backend imports only Bun + node:*.
web-push is optional and lazy (see Web
Push).
Linux and macOS are the supported hosts. The bridge itself also runs on Windows (experimental) against Herdr's Windows beta — see Windows.
On the host, not your phone. Two ways in.
From GitHub (turnkey) — Herdr fetches and builds for you:
herdr plugin install AltanS/collie
herdr plugin action invoke start --plugin herdr.collieFrom a local clone (for development) — registered by path:
git clone https://github.com/AltanS/collie.git && cd collie
herdr plugin link "$(pwd)"
herdr plugin action invoke start --plugin herdr.collieEither way, start does four things:
- builds
web/distif it's missing (typechecked, staged, swapped in atomically), - starts the bridge as the
systemd --userservicecollie(nohupfallback without systemd), - publishes it on the tailnet — literally
tailscale serve --bg 8787: HTTPS on the host's MagicDNS name,:443 → 127.0.0.1:8787, tailnet-only, - prints the banner with the URL to open — walked through line by line in First run.
No Herdr? Run
scripts/collie-ctl.sh startdirectly — same effect (config then lives in~/.config/collie/.env).
The transcripts below are the control script's inline output. Through invoke start you get
Herdr's JSON envelope instead — the same text is the action's captured stdout, read with
herdr plugin log list --plugin herdr.collie.
$ scripts/collie-ctl.sh start
building web UI (first run)… # linked clone only; a GitHub install already built
…bun install · typecheck · vite build output…
bridge started (systemd --user: collie)
tailscale serve (https) → tailnet :443 -> 127.0.0.1:8787
✓ Collie is running · v0.15.0+174c4e4
service systemd --user (collie) · active
local http://127.0.0.1:8787
tailnet https://myhost.tail1234.ts.netThe ✓ is a real probe — the script connected to the bridge's port and got an answer, not just
"the unit is active". If you get ⚠ Collie isn't answering on :8787 yet instead, see
Troubleshooting.
start left three durable things on the host:
web/dist— the built UI, served from disk, so later rebuilds go live without a restart.- A supervised user service — a
systemd --userunit namedcollie, or a launchd agent on macOS (Surviving reboots has the details of both). - A tailnet-only
tailscale servemapping — HTTPS on the host's MagicDNS name,:443 → 127.0.0.1:8787, TLS terminated by Tailscale. Inspect withtailscale serve status; remove just this mapping withscripts/collie-ctl.sh unserve.
stop merely pauses the service; uninstall reverses 2 + 3 and keeps your .env and the checkout.
Why a service and not a Herdr pane: ARCHITECTURE.md §3.
The URL is the banner's tailnet line — print it again anytime with scripts/collie-ctl.sh url, or
scripts/collie-ctl.sh qr to print it as a QR code you can scan. It resolves for any device on your
tailnet, so the phone needs the Tailscale app installed and connected to the same tailnet as the
host.
Then install it as an app: iOS — Safari → share sheet → Add to Home Screen. Android —
Chrome → ⋮ menu → Add to Home screen (or Install app). Installing (and Web Push) needs the
HTTPS origin the default serve mode already provides; over COLLIE_SERVE_MODE=http the page works,
but service worker and install silently no-op.
A sixty-second check, host side then phone side:
$ scripts/collie-ctl.sh status
✓ Collie is running · v0.15.0+174c4e4
service systemd --user (collie) · active
local http://127.0.0.1:8787
tailnet https://myhost.tail1234.ts.net
serve config:
https://myhost.tail1234.ts.net (tailnet only)
|-- / proxy http://127.0.0.1:8787$ scripts/collie-ctl.sh logs # journal timestamps trimmed here
[push] disabled (no VAPID keys configured)
[bridge] listening on http://127.0.0.1:8787 (poll 1500ms)
[bridge] WARNING: COLLIE_TRUSTED_USER is empty — any tailnet device/user that reaches the bridge gets full write access. Set it to your tailnet login (see README → Variant A).
[bridge] WARNING: COLLIE_PUBLIC_HOSTS is empty — Host-header validation is OFF (DNS rebinding not blocked). Set it to your MagicDNS name, especially under COLLIE_SERVE_MODE=http.Both WARNINGs are expected on a fresh install — that's the bridge telling you it's running
open-by-default on your tailnet. Configure closes both. (The loopback URL in the log
is also correct: the bridge itself only ever binds 127.0.0.1 — tailscale serve is what makes it
reachable.) [push] disabled is expected too: notifications are opt-in, and
Web Push is three commands.
On the phone: your agents are listed, and the footer build stamp (v0.9.0 · debcff9 · …) matches
scripts/collie-ctl.sh version. If the page loads but stays empty, that's the same-origin gate —
see Troubleshooting.
Out of the box Collie runs open single-user: anyone on your tailnet who can reach the URL has full control — that's exactly what the two startup WARNINGs are about. Close both in one sitting:
# in your .env
COLLIE_TRUSTED_USER=you@example.com # your tailnet login — the bridge rejects anyone else
COLLIE_PUBLIC_HOSTS=myhost.tail1234.ts.net # exact host(s) you serve on — blocks DNS rebindingConfig is a .env in the plugin's config dir — find it with
herdr plugin config-dir herdr.collie (typically ~/.config/herdr/plugins/config/herdr.collie;
without Herdr, ~/.config/collie). collie-ctl.sh resolves this same dir whether you run it
directly or via a Herdr action:
cp .env.example "$(herdr plugin config-dir herdr.collie)/.env"The bridge reads .env only at startup — after any edit, scripts/collie-ctl.sh restart. See
.env.example for the full option list — commonly COLLIE_PORT, or
COLLIE_SERVE_MODE=http (Headscale / .internal domains; read by the control script when it runs
tailscale serve).
Reading history from more than one agent home? List them all in COLLIE_TRANSCRIPT_ROOT,
comma-separated.
Custom domain or reverse proxy? DEPLOYMENT.md has the full front-door setup.
The one rule to know here: Collie is same-origin only, so a different hostname or TLS terminator
needs the exact origin allowed —
COLLIE_ALLOWED_ORIGINS=https://collie.example.com— and until you do, the page loads and stays empty (Troubleshooting has the symptom).
Commands only this machine has (a plugin's /fork-in-herdr, your own /deploy) go in
commands.toml:
cp commands.toml.example "$(herdr plugin config-dir herdr.collie)/commands.toml"[[commands]]
scope = "omp" # optional; omit for every pane
command = "/fork-in-herdr"
description = "Fork this conversation into a new herdr tab"A pane your rows match shows only your rows (narrowest row wins,
ADR 0018). Add confirm = true for a
two-tap confirm. No restart — edits are live. Verify: open a pane, tap /, your rows are on the
first screen. Syntax error? journalctl --user -u collie -n 20 names the line.
COLLIE_MULTI_SESSION=on (the default) discovers and serves every named Herdr session under your
config root, switchable from the header; COLLIE_MULTI_SESSION=off serves only the primary one. Every
session it finds is drivable through the same URL — including a private or sandbox one, which is why
Security lists this as a sharp edge.
Collie follows your phone by default. To pin it, open Settings → Appearance and pick System, Light or Dark — per device, stored in the browser.
The terminal mirror is the exception: it always renders on a dark ground and light mode inverts it rather than re-colouring it. Agents emit absolute colours chosen for a black terminal, and inverting is what keeps the contrast they designed for (ADR 0002 has the measurement). So keep your agents on a dark theme — a light-themed agent emits dark-on-light colours that are unreadable under either appearance. (Installed on iOS, the status-bar text stays white in light mode; iOS gives web apps no way to change that at runtime.)
Every command works two ways: the control script on the host (scripts/collie-ctl.sh <cmd>) or
the equivalent Herdr action (herdr plugin action invoke <cmd> --plugin herdr.collie, written
below as invoke <cmd>). The ones you'll actually use:
| Action | Control script | Herdr action |
|---|---|---|
| Start — build if needed, serve, print the URL | collie-ctl.sh start |
invoke start |
| Stop — pause the bridge; removes nothing | collie-ctl.sh stop |
invoke stop |
| Restart | collie-ctl.sh restart |
invoke restart |
| Status — the Collie is running banner + URLs | collie-ctl.sh status |
invoke status |
| URL — print the tailnet URL | collie-ctl.sh url |
invoke url |
| QR — the same URL as a scannable code | collie-ctl.sh qr |
— (script only) |
Version — the running version (0.x.y+sha) |
collie-ctl.sh version |
invoke version |
| Update — advance the checkout + rebuild + restart | collie-ctl.sh update |
invoke update |
Uninstall — remove the service; keep .env + checkout |
collie-ctl.sh uninstall |
invoke uninstall |
| Logs — tail the journal / log file | collie-ctl.sh logs |
— (script only) |
Push keys — generate the VAPID keypair into your .env |
collie-ctl.sh push-keys |
invoke push-keys |
| Push test — send one notification to prove it works | collie-ctl.sh push-test |
invoke push-test |
The actions are declared in herdr-plugin.toml and each one shells out to the control script; list
them live with herdr plugin action list --plugin herdr.collie. build · serve · unserve are
script-only too.
start and status end with the Collie is running banner — annotated line by line in
First run. Its version comes from the served bundle stamp, so it is
the authoritative "what's running". Through a Herdr action you get Herdr's JSON envelope, not the
banner — the human-readable output is the action's captured stdout, read with
herdr plugin log list --plugin herdr.collie (or run the control script directly to see it inline).
Pause the bridge without removing anything (a later start brings it right back):
scripts/collie-ctl.sh stop # or: herdr plugin action invoke stop --plugin herdr.collieTo tear the service down completely — stop + disable it, remove the service definition (the
systemd --user unit, or the launchd agent plist on macOS), and remove
Collie's own tailscale serve mapping (port-scoped, so other tailnet mappings on the host survive) —
use uninstall. It leaves your .env and the checkout untouched:
scripts/collie-ctl.sh uninstall # or: herdr plugin action invoke uninstall --plugin herdr.collieThen herdr plugin uninstall herdr.collie (or, for a linked clone, just deleting the directory)
removes the plugin registration itself.
The checkout is the plugin, and Herdr has no plugin update of its own. One command does the lot:
scripts/collie-ctl.sh update # or: herdr plugin action invoke update --plugin herdr.collieIt advances the checkout, rebuilds the UI and restarts the bridge (re-execing itself, so it's safe
even when the update rewrites the script). Confirm via the footer build stamp. Pinned to a version
with --ref? Keep refreshing with herdr plugin install --ref … — update always goes to the
latest.
Fails with "You are not currently on a branch"? That's a GitHub install made before 0.23.1, and Troubleshooting has the one-time repair.
The two install routes differ in when the UI builds — a GitHub install at install time, via the
manifest's [[build]] step; a linked clone on first start.
They also leave two different shapes on disk, which is what update has to cope with.
herdr plugin install doesn't clone: it fetches one commit and detaches onto it, so the checkout has
no branch. A linked clone sits on one, the way you'd expect.
One command handles both (ADR 0006):
- Linked clone (on a branch) —
git pull --ff-only, then re-links the plugin so Herdr picks up any new actions and the new version. herdr plugin install(detached, shallow) — fetches the default-branch tip and re-detaches onto it.--depth 1only if it's already shallow, so a full history is never truncated;--forceso a lockfile the build rewrote can't wedge the next update. It deliberately does not re-link: linking re-registers the plugin as a local path, after which Herdr refusesherdr plugin install— which is your recovery path if this checkout ever breaks.
By hand: frontend (web/) → collie-ctl.sh build (live, no restart — served from disk); backend
(bridge/) → systemctl --user restart collie. Run scripts/install-hooks.sh once to enable the
repo's pre-commit / pre-push checks.
A systemd --user service only runs while you have a login session. On a host that should serve
Collie unattended, enable lingering once:
loginctl enable-linger $USERThe unit is enabled, so with lingering it starts at boot with your user manager; the
tailscale serve mapping is persistent (--bg) and comes back on its own. Inspect the unit with
systemctl --user status collie.
On macOS there's nothing to enable. start installs a launchd agent
(~/Library/LaunchAgents/herdr.collie.plist) with RunAtLoad, so the bridge comes back when you log
in and launchd restarts it if it exits abnormally. Inspect it with
launchctl print gui/$(id -u)/herdr.collie. It's a LaunchAgent, not a daemon, so it starts at
login rather than at boot — a Mac sitting at the login window is not serving Collie. (Neither
supervisor? A nohup process with a pidfile in the config dir instead.)
The bridge always binds loopback only; what changes between deployments is what sits in front
of it and how a request proves who it is. Variant A is the default and sits below; the other four
are in DEPLOYMENT.md. Pick one.
The happy path from Install. tailscale serve terminates TLS on your MagicDNS name and
injects Tailscale-User-Login; set COLLIE_TRUSTED_USER to your tailnet login and the bridge
rejects anyone else.
# in your .env
COLLIE_TRUSTED_USER=you@example.com- Granularity: the tailnet person, not the device.
- Why it's safe on bare
tailscale serve: serve is the trusted injector ofTailscale-User-Login— it sets that header itself and a client can't forge it through the proxy. - Nothing else to configure; origins match automatically on the MagicDNS name.
This is the right choice unless you specifically need per-device control. If you do, or if Tailscale
isn't in the path at all, DEPLOYMENT.md has the rest:
- B — identity-aware proxy, authorised by device — a proxy on this host; some devices drive, others watch.
- C — reverse proxy as the only front door — no Tailscale anywhere in the path.
- D — off-host identity proxy over the tailnet — one central ingress node fronting Collie among your other services.
- E — any other mesh or tunnel — NetBird, ZeroTier, Cloudflare Tunnel: you own the ingress, Collie publishes nothing.
The bridge runs on Windows against Herdr's Windows beta; the launcher does not. Herdr there
exposes its control socket as a named pipe named after the full socket path, not an AF_UNIX
socket, so Collie dials it through node:net instead of Bun.connect — one shim,
bridge/dial.ts, which explains the mapping at the top of the file.
What that means in practice:
- Run the bridge directly —
bun run bridge/index.ts. There's no systemd unit, and the Herdr action buttons shell out tobash, so they only work if Git Bash is onPATH. The manifest therefore still declareslinux/macosonly, rather than advertising buttons that may not fire. tailscale serveisn't wired up here. Use the Variant C posture: loopback bind, your own ingress in front,COLLIE_PUBLIC_HOSTSpinned. The security rules in §Security are not relaxed on Windows.- Set
COLLIE_MULTI_SESSION=off— session discovery derives POSIX paths. - The socket path defaults to
%APPDATA%\herdr\herdr.sock; override withHERDR_SOCKET_PATH(an explicit\\.\pipe\…value is passed through untouched).
Want the lifecycle too? The bridge has spoken Windows' named pipe since 0.15.0; a
community-maintained Task Scheduler setup (start/stop/update, no supported-tree guarantees) lives in
contrib/windows/.
Is it actually working? The bridge logs [events] stream up on start — the event stream works
over the pipe, so Windows gets the same live updates as Linux, not degraded polling.
COLLIE_HERDR_DIAL=net forces that same dialer on Linux/macOS. It exists so the Windows code path
can be exercised — and regression-tested — without a Windows box; bridge/dial.test.ts uses it.
Off unless you opt in. Three steps, and nothing to install — the sender (web-push) is already an
optional dependency, installed by the build:
herdr plugin action invoke push-keys --plugin herdr.collie # 1. generate + write the VAPID keys
herdr plugin action invoke restart --plugin herdr.collie # 2. the bridge reads them at start
# 3. on your phone: Settings → notificationsStep 1 is the one that used to be fiddly. push-keys generates the keypair and writes
COLLIE_VAPID_PUBLIC / _PRIVATE into the .env the service actually reads, at mode 600.
Worth one extra keystroke: pass a subject — the contact address RFC 8292 wants, so a push service has a way to reach whoever is sending. An action carries no arguments, so this form is the shell one:
bash scripts/collie-ctl.sh push-keys mailto:you@example.comTwo behaviours worth knowing. It refuses to replace keys that are already live unless you pass
--force, because new keys invalidate every existing subscription: each device must re-enable
notifications, and until it does it silently receives nothing. But passing a subject on an
already-configured install is not that — it updates the contact address and leaves the keys alone,
so fixing a typo never costs you your subscribers.
On a Herdr install older than 0.8.0, actions are the set cached when the plugin was installed (ADR 0006), so
push-keysandpush-testwon't appear until the nextherdr plugin install. Usebash scripts/collie-ctl.sh push-keysuntil then — it does the identical thing.
Did it work? Fire a notification at every subscribed device without waiting for an agent to block:
bash scripts/collie-ctl.sh push-test # or: push-test "Title" "Body"You should get it within a second or two. If it says push is disabled, the bridge didn't get the keys — restart it (step 2). If it says there are no subscribed devices, step 3 hasn't happened on that phone yet.
Push needs a secure context (HTTPS), which any HTTPS-terminating front door provides — the
default tailscale serve (Tailscale manages the MagicDNS cert; nothing to obtain or renew) or a
Variant C proxy that
terminates TLS. Plain-HTTP modes (COLLIE_SERVE_MODE=http) are not a secure context, so the
browser won't even offer the subscribe button — Settings flags it insecure.
Collie pushes when an agent goes blocked or done, with the agent's message in the body; tapping it opens Collie at that agent.
Symptoms below, in order — search the page for yours. Os { NotFound } from herdr plugin ·
update says "not currently on a branch" · tailscale serve failed · isn't answering
(service won't start) · phone can't open the URL · page loads but stays empty (blank page,
403) · a password prompt won't take your reply · no push notifications · gone after a
reboot · herdr plugin list shows the old version · stale UI after a rebuild.
herdr plugin … fails with Error: Os { code: 2, kind: NotFound, message: "No such file or directory" } (plugin install fails, action invoke fails). This is not a Collie problem — it
means the Herdr server isn't running, so its CLI can't reach the control socket
(~/.config/herdr/herdr.sock). The tell is the raw Os {…}
error: a reachable server answers path/manifest problems with structured JSON (e.g.
plugin_manifest_not_found), so a bare Os { NotFound } is a failed socket connect, before Collie
or your path is ever examined. It hits link, install, action invoke — every subcommand that
talks to the server — while herdr plugin --help still works (it never opens the socket). Fix: start
Herdr first (herdr server &, or just launch the Herdr TUI — it boots the server), confirm
ls ~/.config/herdr/herdr.sock now exists, then retry the install. herdr plugin list is a quick
probe: if it throws the same error, the server is down.
update fails with You are not currently on a branch. A GitHub install made before 0.23.1
(#63). herdr plugin install fetches one commit and
detaches onto it rather than cloning, so the old update — which ran git pull — had no branch to
pull into, and no install of that vintage could refresh itself. The fix ships inside the checkout it
repairs, so it takes one reinstall to land; update works normally from then on:
herdr plugin install AltanS/collie --yes # replaces the checkout, rebuilds the UI
herdr plugin action invoke restart --plugin herdr.collie # reinstall doesn't restart the service
herdr plugin action invoke version --plugin herdr.collie # expect 0.23.1 or newerYour .env and tailscale serve state live in the plugin config dir, outside the checkout, so they
survive.
start prints note: tailscale serve failed. The bridge itself is fine (still up on
127.0.0.1) — only the tailnet ingress didn't come up, and the script prints tailscale's own error
right below the note. Usual causes: your user isn't the Tailscale operator
(sudo tailscale set --operator=$USER), the node is logged out (tailscale up), or — on
Headscale / .internal tailnet domains — HTTPS certs aren't available, which is exactly what
COLLIE_SERVE_MODE=http is for: set it in .env, then scripts/collie-ctl.sh restart. Verify with
tailscale serve status.
Banner shows ⚠ Collie isn't answering on :8787 yet (service won't start, connection
refused). The service was started but the HTTP server isn't answering the probe. Check the unit
first — systemctl --user status collie — then scripts/collie-ctl.sh logs (or
journalctl --user -u collie -f to watch live) for why: most commonly the port is already taken
(set COLLIE_PORT in .env, then scripts/collie-ctl.sh restart, which also re-runs
tailscale serve against the new port) or the first build failed (the log says so; fix and run scripts/collie-ctl.sh build). The unit
auto-restarts every 5 s, so once the cause is fixed it usually comes back on its own.
Phone can't open the tailnet URL. Work down the list: (1) the phone runs the Tailscale app and
is connected to the same tailnet as the host; (2) you're opening the banner's tailnet URL
(scripts/collie-ctl.sh url), not the local one — http://127.0.0.1:8787 only works on the host
itself; (3) MagicDNS is enabled in your tailnet's DNS settings (the URL is a MagicDNS name); (4) the
host is online — check tailscale status on the host, or ping the host from the phone's Tailscale
app; (5) your tailnet policy actually admits a peer to this node — if it doesn't, the banner now
says so under the tailnet line, and nothing else will: the front door is published correctly, the
cert is valid, and curl from the host itself returns 200, because loopback never touches the packet
filter. Two things make this one especially misleading — tailscale ping succeeds (disco pings
bypass ACLs), and blocked traffic is dropped rather than refused, so the phone just hangs and reads
as "server down". Fix it in your ACL policy (https://login.tailscale.com/admin/acls on Tailscale;
your policy file on Headscale). The check is best-effort and deliberately unsure of itself: it speaks
up only when this node's filter admits nothing — which can equally mean no other device has joined
the tailnet yet — and stays quiet whenever it can't tell.
Page loads but stays empty (blank page, white screen); API calls fail
403 cross-origin rejected. You're reaching Collie through an origin the bridge doesn't expect — a
custom domain, or a proxy that rewrites Host. Allow the exact public origin with
COLLIE_ALLOWED_ORIGINS (see Configure), or make the proxy forward Host unchanged —
the fourth proxy requirement in
DEPLOYMENT.md.
A sudo (or SSH passphrase, or gpg) prompt won't take your reply. Use Type in the
Controls row, not Send. Send verifies what it typed by reading it back off the screen before it
presses Enter (#34), and a password prompt turns echo
off, so there is nothing to read back — Type sends your keystrokes straight to the pane, Enter
included. Nothing you type in Type is stored, echoed into a draft, or restored later, and the
moment Collie recognises a password prompt it drops the stored draft too
(#103).
No push notifications arriving. Fire one by hand: bash scripts/collie-ctl.sh push-test. Three
causes, in the order the command distinguishes them:
push says it's disabled (the keys never reached the bridge — run push-keys and restart, see
Web Push); it says there are no subscribed devices (this phone never enabled
them in Settings → notifications); or it reports a send and nothing arrives (the phone is on a
plain-HTTP origin, which is not a secure context — Settings flags it insecure).
Collie is gone after a reboot. On Linux this is almost always lingering — see
Surviving reboots for the one command. On macOS the launchd agent starts at
login, so check you're actually logged in (not sitting at the login window) and that the agent is
loaded: launchctl print gui/$(id -u)/herdr.collie.
herdr plugin list shows the old version after an update. Expected — Herdr caches the manifest
it read at install or link time. The authority on what's running is the footer build stamp, or
scripts/collie-ctl.sh version. For a linked clone update re-links and that self-heals (force it
with herdr plugin link "$(pwd)"); on Herdr ≥0.8.0 the manifest is re-read from disk anyway.
Phone shows a stale UI after a rebuild. A PWA's service-worker cache is per-origin, so reaching
Collie at two origins (a custom domain and the raw host:8787) gives you two installs, each
caching its own bundle. The footer build stamp (vX.Y.Z · sha · time) shows the bundle you're
running; the bridge reports what it serves via the X-Collie-Build header and /api/config. On a
mismatch, the footer offers "new build — tap to update." Otherwise reopen the PWA a couple times
(the SW auto-updates) or clear that origin's site data. Best practice: pick one HTTPS origin and
stick to it. (Over plain HTTP the SW can't register — always fresh, but no PWA features.)
A small Bun process sits between your phone and Herdr — the browser never touches the socket.
phone (PWA)
│ HTTPS over the tailnet
▼
tailscale serve terminates TLS, injects the identity header
│ 127.0.0.1:PORT (the bridge binds loopback only)
▼
Collie bridge (Bun) serves the UI + a small JSON API; polls Herdr
│ one-shot JSON-RPC over a Unix socket
▼
Herdr server owns the panes, agents and terminal state
Under Variant C a
reverse proxy replaces the tailscale serve box; everything below the front door is identical.
- One module touches the socket (
bridge/herdr-client.ts); everything else speaks the bridge's HTTP API. - Polling is still the model — the bridge polls Herdr (via
session.snapshot, one RPC per tick) and the browser polls/api/snapshot; a long-lived Herdr event stream only pokes the bridge's poll to go faster, it never replaces it. No resync logic. - Actions are plain HTTP — a reply or key
POSTs to/api/pane/:id/{reply,keys}→ Herdrpane.send_keys, which types into a real terminal (hence the security posture). - The UI is a static PWA — Vite builds
web/dist, served from disk, so a rebuild is live with no restart.
Full design rationale in ARCHITECTURE.md.
Clone it and herdr plugin link it (Install above), then edit in place.
- The manifest is the plugin.
herdr-plugin.tomldeclares the actions listed in Commands, and each one shells out toscripts/collie-ctl.sh. Both are commented — read them, not a paraphrase of them here. - One asymmetry in the dev loop:
web/rebuilds go live with no restart (the bridge servesweb/distfrom disk);bridge/changes needsystemctl --user restart collie. Build, test and versioning rules are inCLAUDE.md— versioning is hook-enforced, so skim it before your first commit. - Why a supervised service and not a plugin pane —
ARCHITECTURE.md§3. That decision is why the manifest uses[[actions]]and[[build]]and nothing else.
Herdr's plugin system itself is upstream's to document: authoring · CLI reference · example plugins.
- Deployment variants B–E —
DEPLOYMENT.md - Design & rationale —
ARCHITECTURE.md - Verified Herdr socket API —
HERDR_API.md - Ops, versioning & conventions —
CLAUDE.md - Changes —
CHANGELOG.md
In the works: more than one machine under a single URL — one Collie leads, the others join it.






