Web UI for herdr — drive Herdr servers from your phone.
One herdweb process attaches to Herdr servers:
the default server on this machine, other named servers on the same machine, and
servers on other machines via herdr --remote.
The phone path is built for weak networks, one-handed controls, voice, and
attention that reaches you even when the page is backgrounded.
It is a self-hosted fork of connorads/remobi — independent since 2026-08-20. See the fork decision.
Herdweb uses Herdr's words, then adds one of its own:
| Word | Meaning |
|---|---|
| Server | Herdr background process that owns panes. A named session is a separate server namespace on the same machine. |
| Device | A machine running Herdr — this laptop, a workbox, anything herdr --remote can reach. |
| Target | One herdweb spawn command that attaches to one server: herdr --session … or herdr --remote …. |
| herdweb | One process that can hold several targets. The browser attaches to one target at a time. |
flowchart LR
Phone[Phone or desktop]
HW[herdweb]
A["herdr --session default"]
B["herdr --session work"]
C["herdr --remote workbox"]
Phone --> HW
HW --> A
HW --> B
HW --> C
The picker is a flat list of targets. Two servers on one device are two rows, not a device → server submenu. Runtime details live in How herdweb works.
| Shape | Herdr fact | What you run |
|---|---|---|
| Single device, one server | One machine, default session | herdweb serve — no picker |
| Single device, several servers | Named sessions on this machine | targets with herdr --session … |
| Several devices | Herdr servers on other machines | Add herdr --remote <host> rows |
Opening the same herdweb URL on a phone and a laptop shares the live target. That is a client of herdweb, not a Herdr device.
These are the optimizations the product is built around — not extras on a generic terminal.
- Weak networks — a drop marks the screen stale and blocks replay until a fresh snapshot lands. Composer drafts stay on the phone; heartbeat + backoff reconnect; returning to the tab always resyncs instead of trusting an old OPEN socket. See Networking.
- Phone control — the soft keyboard stays down unless you tap ⌨. The toolbar lifts above the IME (
--kb-inset). 44px taps, haptics, floating d-pad, swipe between herdr tabs, pinch to zoom. Soft keyboard is an escape hatch, not the default input path. - Voice input — toolbar opens a second-layer composer; Mic is tap-to-toggle. Edit the transcript before send. Draft and pending send survive disconnect. Needs HTTPS on a phone and a private
.localkey. See Configuration — Voice. - Many Herdr servers — one page switches local named sessions and
herdr --remotemachines from a flat badge. One WebSocket, one committed attachment. See Three shapes. - PWA notifications — add to the Home Screen, subscribe from ☰ → 🔔. Web Push fires on silence, process exit / restart, and tests. A tap focuses the open page and selects that target (iOS only works as a standalone PWA). See Configuration — Push.
- Webhook notifications —
notify.channelsposts the same events in parallel to message-pusher, WeCom, or a raw webhook. Use this when the phone cannot reach FCM; a failed channel does not block Web Push or the others.
Also: defaults match herdr keybindings, and the process stays on localhost until you publish it through a layer you trust.
This is single device, one server: default Herdr session on this machine.
git clone <your-fork-url> herdweb && cd herdweb
pnpm install
git config core.hooksPath .hk-hooks # enable commit hooks (conventional commits, biome)
# Start (attaches to herdr --session default, serves on 127.0.0.1:7681)
pnpm exec tsx cli.ts serveOr build first, then run from dist/:
pnpm run build:dist
node dist/cli.mjs serveOpen http://localhost:7681 on the same machine to verify. For phone access,
deploy behind a trusted proxy or tunnel — see Deploying herdweb.
herdr captures mouse input by default, so touch scroll and tap-to-focus work with no extra multiplexer configuration.
If you already run herdr, you can install herdweb as a plugin instead of cloning by hand:
herdr plugin install zlxlabs/herdweb
herdr plugin action invoke zlxlabs.herdweb.startinstall clones this repository, runs a preflight check, builds, and registers
the plugin. start launches herdweb in a herdr tab; the server's lifetime is
that pane's.
The git clone path in Quick start still works. Both paths are
valid.
Node.js ≥ 22 is required on every platform — see Requirements.
macOS. The node-pty npm package ships darwin prebuilds, so install usually
does not need a local compile.
Linux. That package ships no linux prebuilds. Install therefore compiles
node-pty on the machine, which needs Python 3, make, and a C++ compiler:
| Distro | Command |
|---|---|
| Debian/Ubuntu | sudo apt install python3 make g++ |
| Fedora | sudo dnf install python3 make gcc-c++ |
| Arch | sudo pacman -S python make gcc |
| Alpine | sudo apk add python3 make g++ |
The plugin checks for these before it builds and names anything missing, so you are not left guessing from a long npm log.
At runtime (both platforms) the plugin needs either perl or python3 to take
the flock that keeps a single herdweb serving at a time. macOS and every distro
above ship at least one of them, so this normally needs no action — but it is a
runtime dependency, separate from the build toolchain above, and doctor reports
the two separately.
The plugin manifest does not declare keybindings. To add a shortcut, paste this
into your own herdr config.toml — it is optional:
[[keys.command]]
key = "prefix+u" # example; prefix+w is already herdr's workspace_picker — do not use it
type = "plugin_action"
command = "zlxlabs.herdweb.show"
description = "show herdweb URL"Without a shortcut, this still works:
herdr plugin action invoke zlxlabs.herdweb.showSingle device, several servers — each named session is its own Herdr server:
export default {
defaultTargetId: 'local',
targets: [
{ id: 'local', name: 'Local', command: ['herdr', '--session', 'default'], imageDrop: 'local-path' },
{ id: 'dev', name: 'Local · Dev', command: ['herdr', '--session', 'herdweb-dev'], imageDrop: 'local-path' },
],
}Several devices — add a remote Herdr server as another flat row:
export default {
defaultTargetId: 'local',
targets: [
{ id: 'local', name: 'Local', command: ['herdr', '--session', 'default'], imageDrop: 'local-path' },
{ id: 'workbox', name: 'Workbox', command: ['herdr', '--remote', 'workbox'], imageDrop: 'disabled' },
],
}A picker appears only when there is more than one target. Explicit configs must
set defaultTargetId and must not pass a trailing command after --. Full
rules: Configuration — Targets and
Deploying herdweb.
herdr --remote is a local thin client over SSH. herdweb can tell that this
local process started or exited; it cannot tell whether a remote pane is healthy.
The herdweb-setup skill checks your
environment, interviews you about your workflow, generates a validated
herdweb.config.ts, and walks through deployment — one conversation.
Tell your coding agent:
Read
.agents/skills/herdweb-setup/SKILL.mdin this repo and follow it to onboard me.
herdweb is a remote-control surface for your terminal. Anyone who can reach it can drive the attached Herdr server with your user privileges.
herdweb servebinds to127.0.0.1by default.- Plugin-installed herdweb also binds
127.0.0.1by default. The plugin does not provide any one-click way to expose it on the network; phone access still goes through a trusted tunnel or VPN. - The inner PTY-backed session stays local to the herdweb process.
- There is no built-in login, password, or ACL in herdweb itself.
- Safe default: keep it on localhost and publish it through a trusted layer like Tailscale Serve.
- If you use
herdweb serve --host 0.0.0.0, you are exposing terminal control to your LAN / whatever can route to that port. Do that only if you intentionally want direct network exposure and have separate network controls in place.
To report a vulnerability, see SECURITY.md.
Buttons, gestures, voice, image drop, and push notifications live in
Configuration. Config search order, .local secrets,
and the remobi-migration trap are there too.
Voice capture needs a secure context (HTTPS on a phone; localhost is fine).
A plain HTTP LAN address is not — herdweb hides the control rather than showing
a dead mic.
The default bottom toolbar carries only general terminal keys. The agent-only
Ctrl+] button lives in the drawer's Agent section, right below Reply.
One tap writes a single 0x1d byte (Ctrl+]) to the agent's PTY; a second tap
writes it again.
Agents that bind Ctrl+] — Pi's ask_user_question plugin, for one — use that
byte to collapse the question card so you can read and scroll the context
underneath it. The answers you already picked are kept: tapping Ctrl+] again
brings the same card back with your selections intact.
It is not a hide switch, and it is not stateful: herdweb cannot see whether
a question is open, so it only ever sends the byte and lets the agent decide.
Cancelling a question is a different key — Ctrl+[ or Esc — and neither is
what this button sends. With no question on screen, the byte reaches the agent
like any other keystroke.
herdweb serve [--config <path>] [--port <n>] [--host <addr>] [--base-path <path>] [-- <command...>]
Start herdweb with its built-in web terminal and PWA support.
Default host: 127.0.0.1. Default port: 7681. Single default command: herdr --session default
Example: herdweb serve --host 0.0.0.0 --port 8080
Example: herdweb serve --base-path /random-token
Example: herdweb serve --port 8080 -- herdr --session dev
herdweb build [--config <path>] [--output <path>] [--dry-run]
Deprecated. herdweb no longer patches ttyd HTML.
herdweb inject [--config <path>] [--dry-run]
Deprecated. herdweb no longer patches ttyd HTML.
herdweb init
Scaffold a herdweb.config.ts with commented defaults.
herdweb --version
herdweb --help
Short flags: -c (--config), -p (--port). Legacy deprecated flags:
-o (--output), -n (--dry-run).
The -- escape hatch after serve overrides the command only in single mode,
for example herdweb serve -- bash --norc. Explicit targets configs must not
use a trailing command after --.
| Kind | Where |
|---|---|
| Start | This README |
| How-to | Deploy · Configuration · Tailscale Serve · Mobile panes · Keep awake |
| Explain | How herdweb works · Networking · Herdr concepts |
git clone <your-fork-url> herdweb && cd herdweb
pnpm install
git config core.hooksPath .hk-hooksFrom source (bundles the browser client on the fly via esbuild — no build step):
pnpm exec tsx cli.ts serve # localhost:7681, default herdr sessionpnpm test # vitest (unit + integration)
pnpm run test:pw # playwright e2e (needs: pnpm exec playwright install chromium webkit --with-deps)
pnpm run check # biome lint + formatmainpublishes stable GitHub Releasesdevpublishes prereleases- merge
devintomainto promote an experimental line to stable
This fork does not publish to npm. Versioning and changelog are driven by
semantic-release on push to main and dev.
What is a target?
One spawn command that attaches to one Herdr server. Local named sessions and
herdr --remote hosts are all targets. See Three shapes.
Does herdr --remote mean the remote pane is healthy?
No. It only proves the local thin-client process. See
How to work with Herdr.
Can a phone and a laptop share the same server? Yes. Open the same herdweb URL. They share that target's live session. Switching targets in one browser does not switch the other.
Is this secure? herdweb doesn't handle auth — it's a UI overlay. Use a tunnel or VPN you trust. See Deploying herdweb and Tailscale Serve. Security is your responsibility.
Why not Termux / Termius / SSH apps? They work, but you're managing SSH keys and fighting a UI that wasn't built for touch. herdweb keeps your herdr workflow and adds touch controls on top.
Why not chat-based mobile apps? Those tools change your workflow. herdweb gives you the raw terminal — full power, self-hosted, works with every agent herdr can host.
Is this production-ready? It's early. The author uses it daily. Feedback welcome.
Android shows no install prompt when herdweb is behind Cloudflare Access?
Chromium's PWA installability check fetches manifest.json separately, and the
<link rel="manifest"> fetch sends no cookies by default. Behind Cloudflare
Access (or any authenticating proxy) that fetch gets redirected to the login
page, the check fails, and no install UI appears. Since v1.6.1 the manifest
link carries crossorigin="use-credentials" so the fetch includes cookies —
but only if the proxy lets credentialed requests through. The reliable fix is
to bypass auth for these five paths at the proxy (Cloudflare Access: create a
self-hosted application matching the paths with a Bypass + Everyone policy;
Access matches the most specific path first, so the rest of the site still
requires login): /manifest.json, /sw.js, /icon-192.png, /icon-512.png,
/apple-touch-icon.png. These paths only expose the app name and icons. If
you serve with --base-path, prefix the paths accordingly. iOS "Add to Home
Screen" never validates the manifest or service worker, which is why only
Android is affected.
MIT