herdr-phone

module
v0.2.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 25, 2026 License: MIT

README

herdr-phone

CI Release Go 1.26 cloudflared

Herdr Phone lets one authenticated operator supervise the Herdr session running on their Mac from a phone. An attention-first inbox shows which coding agents need you, which are working, and which changed while you were away. Start a scoped agent run, send instructions, inspect truthful observed output, manage workspaces, and open the full console only when direct terminal control is necessary.

It is a Go 1.26 relay with a React + TypeScript PWA embedded into the binary. It starts and supervises cloudflared, binds its origin to loopback only, and never exposes Herdr's local socket to a browser. Named tunnels use Cloudflare Access as the edge identity layer, and the origin re-validates the Access JWT on every request. Quick Tunnels require explicit opt-in.

⚠️ This is remote shell access, by design

Herdr Phone puts an interactive terminal for your Mac on the public internet. Anyone who reaches the front door and clears its authentication can run arbitrary commands as your user — the same power as an SSH session. Its security bar is that of an SSH client, not a read-only dashboard. Treat the public URL, your Cloudflare Access identity, and the pairing link like a root login. Read SECURITY.md before you expose it.

Contents

Features

  • Agent-first inbox. Runs are grouped as Needs you, Working, Updated, Idle, and Status unknown. Opening a run never changes focus on the Mac.
  • Run control, not terminal cosplay. Send an instruction, retain drafts through connection failures, distinguish rejection from uncertain delivery, and never retry a possibly-delivered instruction automatically.
  • Truthful observed output. A versioned, generation-bound run API returns identity, context, status, and bounded terminal output. Terminal bytes are always labelled as recent output, never fabricated into assistant messages, tool calls, approvals, diffs, or test results.
  • Start run. Choose an existing workspace, create a workspace, or branch a linked worktree; select a server-discovered agent kind; then see every launch step and recover from partial success without undoing valid resources.
  • Workspace management. Inspect worktree provenance, tabs, panes, agents, and lifecycle generations, with complete advanced move, split, resize, zoom, swap, rename, focus, and confirmed-close controls.
  • Full console fallback. A lazy-loaded xterm.js console is one tap away for blocked, unknown, or recovery states. It resizes, scrolls, reconnects, detects pane replacement, and requires explicit confirmation to take over input.
  • Safe structural controls. Create, rename, move, resize, zoom, split, swap, and confirmed-close, each with an explicit, single-use server confirmation for destructive actions.
  • Secure by default. Loopback-only origin, mandatory pairing in every mode, Cloudflare Access with origin-side JWT validation for named tunnels, strict Origin/CSRF/CSP, and terminal escape-sequence filtering.
  • Self-contained. One static binary with the PWA embedded — no runtime CDN, no analytics, no telemetry, no hidden network access.
  • macOS, amd64 and arm64 for v0.2.0.

How it works

 phone (installed PWA)
    │  https / wss  (public hostname)
    ▼
 Cloudflare edge  ── Access (named mode): signed Cf-Access-Jwt-Assertion
    │  outbound-only tunnel (no inbound ports opened on your Mac)
    ▼
 cloudflared (child process, supervised by the daemon)
    │  http / ws to 127.0.0.1:PORT  (loopback only)
    ▼
 herdr-phone serve (Go daemon)
    ├─ validates Access JWT + app session on every request
    ├─ state engine: snapshot as truth, events as wakeups
    ├─ terminal bridge: `herdr terminal session control <pane>` per WebSocket
    └─ typed Herdr socket adapter (the only owner of Herdr wire names)
    │  newline-delimited JSON over the Herdr Unix socket
    ▼
 Herdr server (your panes, tabs, workspaces, agents)

The browser never touches Herdr's Unix socket. The daemon owns every child process and tears them down on exit; killing the daemon tears down the tunnel.

Prerequisites

  • macOS (Apple silicon or Intel). v0.2.0 is macOS-only.
  • Herdr v0.7.5+ with a working plugin command (verify with herdr plugin).
  • cloudflared. It is never installed automatically. Install it with Homebrew (brew install cloudflared) or from Cloudflare's releases; herdr-phone doctor prints exact guidance.
  • A Cloudflare account with Zero Trust enabled, for a named tunnel and an Access application. (Quick Tunnels need no account but are testing-only — see below.)
  • Optional for building from source: Go 1.26.5 or newer (the 1.26 series; 1.26.5+ specifically, because earlier 1.26 patch releases contain reachable standard-library vulnerabilities — the build script treats an older 1.26.x as incompatible and falls back to a checksum-verified release download) and Node.js 22.23.1 (the version pinned in mise.toml; the frontend's Vite 7 requires Node ≥ 22.12, so building from source needs Node 22.12+ on the 22 line or 24+). Prebuilt releases need neither.

Install

herdr plugin install matheus3301/herdr-phone

At install time the plugin builds bin/herdr-phone from source when compatible Go and Node toolchains are present — building the embedded frontend and linking it into one static binary — and otherwise downloads and verifies the SHA-256 checksum of the macOS release archive for your architecture. It never runs curl | sh and never installs cloudflared.

The plugin registers global actions (start, start-quick, stop, status, setup-link, doctor) and installs no default keybinding and no long-running pane.

Verifying a downloaded release

Every release publishes macOS .tar.gz archives, a checksums.txt, an SBOM, and a signed build-provenance attestation.

  • Checksum (mandatory, automatic). When it cannot build from source, the install script downloads the release archive and verifies its SHA-256 against checksums.txt before installing, failing closed on any mismatch. To check an archive yourself:

    shasum -a 256 -c checksums.txt
    
  • Build-provenance attestation (recommended). Each archive and the checksums.txt is attested with GitHub's keyless build provenance (Sigstore/OIDC — no long-lived signing key). Confirm an artifact was built by this repository's release workflow, not swapped after the fact:

    gh attestation verify herdr-phone_0.2.0_darwin_arm64.tar.gz \
      --repo matheus3301/herdr-phone
    

    This checks the file's digest against a provenance statement produced by the herdr-phone release workflow on GitHub, with no trust placed in any downloaded key. Checksum verification stays mandatory and independent of this step.

Choosing a front door

Herdr Phone supports two front doors. Named tunnels with Cloudflare Access are the default and the only mode appropriate for real use. Quick Tunnels are for local testing and are off unless you explicitly enable them.

Named tunnel + Access Quick Tunnel
Edge identity Cloudflare Access (deny-by-default) None
Hostname Stable, yours Random *.trycloudflare.com, changes each run
Uptime Production-grade No SLA; testing only
App pairing Required Required
Default ✅ enabled ❌ off; explicit opt-in

Herdr Phone does not provision anything in your Cloudflare account (that is a deliberate non-goal). You create the tunnel and the Access application once in the Cloudflare dashboard, then point Herdr Phone at them.

  1. Create a remotely-managed tunnel. In the Zero Trust dashboard, go to Networks → Tunnels → Create a tunnel → Cloudflared. Give it a name and copy the tunnel token (you will store it securely — see Providing the tunnel token).

  2. Add a public hostname to the tunnel that routes to the loopback origin Herdr Phone binds:

    • Subdomain/domain: e.g. herdr.example.com
    • Service: HTTP127.0.0.1:8787 (match server.port in your config)
  3. Create a self-hosted Access application for that hostname (Access → Applications → Add an application → Self-hosted). Access applications are deny-by-default; add an Allow policy that matches only your identity (for example, your email). Note two values from the application:

    • the Application Audience (AUD) tag, and
    • your team domain (your-team.cloudflareaccess.com).
  4. Configure Herdr Phone (see Configuration):

    [server]
    host = "127.0.0.1"
    port = 8787
    
    [cloudflare]
    mode = "named"
    public_url = "https://herdr.example.com"
    # Provide exactly one credential strategy — token file, token command, or a
    # locally-managed config/credentials pair. See "Providing the tunnel token".
    token_file = "~/.config/herdr-phone/tunnel.token"
    
    [auth.access]
    enabled = true
    team_domain = "your-team.cloudflareaccess.com"
    audience = "your-application-audience-tag"
    allowed_identities = ["you@example.com"]
    
  5. Start it:

    herdr-phone start
    

    The daemon validates the config, starts cloudflared, waits for readiness, and prints an authenticated pairing URL.

Origin JWT configuration

Cloudflare Access is an edge control. Herdr Phone does not trust it blindly: in named mode the origin cryptographically validates the Access JWT on every HTTP request and WebSocket handshake, so a request that somehow reaches loopback directly is still rejected without a valid token.

Set these under [auth.access]:

  • enabled = true — required in named mode.
  • team_domain — your *.cloudflareaccess.com domain. Used to derive the JWKS endpoint (https://<team_domain>/cdn-cgi/access/certs) and the expected issuer.
  • audience — the exact Application Audience (AUD) tag of your Access application. The origin enforces aud equality.
  • allowed_identities — optional. When non-empty, the verified email (or common_name for service tokens) claim must match one of these exactly.
  • jwks_ttl — how long signing keys are cached (default 1h), with a bounded stale-key fallback and single-flight refresh.

The origin reads only the signed Cf-Access-Jwt-Assertion header, accepts RS256 only, verifies kid/issuer/audience/exp/nbf/iat, and fails closed when JWKS is unavailable and no valid cached key exists. It never trusts the convenience Cf-Access-Authenticated-User-Email header.

Quick Tunnel (testing only, opt-in)

A Quick Tunnel (cloudflared tunnel --url http://127.0.0.1:PORT) needs no Cloudflare account and prints a random *.trycloudflare.com hostname. It has no Cloudflare Access identity at the edge, no uptime guarantee, and is intended by Cloudflare for testing and development only. Herdr Phone still requires app pairing for a Quick Tunnel, but pairing is the only barrier — there is no edge identity in front of it.

Because of that, Quick Tunnels are off by default. To use one for local testing you must both set quick_enabled = true in config and start with the quick flag:

[cloudflare]
mode = "quick"
quick_enabled = true
herdr-phone start --quick

Quick mode ignores Access configuration and shows Quick Tunnel operator in the audit UI. Before it prints a pairing link, Herdr Phone verifies that the public URL reaches this exact instance. Do not use a Quick Tunnel as a durable endpoint, and do not use it for anything you would not expose over an unauthenticated URL.

Providing the tunnel token

A named tunnel needs a credential. Provide exactly one strategy under [cloudflare]; the token is never placed on the command line.

Token file — a file containing the tunnel token, mode 0600, owned by you:

[cloudflare]
token_file = "~/.config/herdr-phone/tunnel.token"

Token command (recommended) — an argv array run directly (never through a shell); its bounded output is trimmed once and written only to a temporary 0600 file that is deleted immediately after cloudflared reads it. Store the token in the macOS Keychain and reference it:

security add-generic-password -a "$USER" -s herdr-phone-tunnel -w "your-tunnel-token"
[cloudflare]
token_command = ["security", "find-generic-password", "-s", "herdr-phone-tunnel", "-w"]

Or with the 1Password CLI:

[cloudflare]
token_command = ["op", "read", "op://Private/Herdr Phone Tunnel/token"]

Locally-managed tunnel — point at a cloudflared config and credentials file plus a tunnel name or UUID:

[cloudflare]
config_file = "~/.cloudflared/config.yml"
credentials_file = "~/.cloudflared/<UUID>.json"
tunnel = "<name-or-uuid>"

Credential and token files must be regular files owned by you and not readable by group or other. No token is ever written to config, logs, state, or argv.

Configuration

Configuration is strict TOML. Unknown keys are errors. It is loaded from the first of:

  1. $HERDR_PLUGIN_CONFIG_DIR/config.toml
  2. $XDG_CONFIG_HOME/herdr-phone/config.toml
  3. $HOME/.config/herdr-phone/config.toml

Find the exact directory Herdr uses for this plugin:

herdr plugin config-dir matheus3301.phone

~ and explicit environment variables are expanded (an unset variable is an error); a shell is never executed. See config.example.toml for the full commented reference.

[server]
host = "127.0.0.1"           # must be exactly 127.0.0.1 in production
port = 8787                  # 1–65535 and must be free
session_ttl = "12h"
idle_lock = "30m"
allowed_workspace_roots = ["~"]

[cloudflare]
mode = "named"               # "named" or "quick"
binary = "cloudflared"
public_url = "https://herdr.example.com"
config_file = ""
tunnel = ""
credentials_file = ""
token_file = ""
token_command = []
quick_enabled = false
grace_period = "15s"

[auth.access]
enabled = true               # required in named mode
team_domain = "your-team.cloudflareaccess.com"
audience = ""
allowed_identities = []
jwks_ttl = "1h"

[herdr]
socket_path = ""             # resolved from config, then HERDR_SOCKET_PATH, then default
binary = ""                  # resolved from config, then HERDR_BIN_PATH, then `herdr` on PATH
poll_hot = "1500ms"          # at least 250ms
poll_cold = "12s"

[ui]
theme = "system"
terminal_font_size = 13

Validation highlights:

  • server.host must be exactly 127.0.0.1 in production; the port must be free.
  • Named mode requires an HTTPS public_url, auth.access.enabled = true, and exactly one credential strategy (config/credentials, token file, or token command).
  • Quick mode requires quick_enabled = true, ignores Access configuration, and still requires pairing.
  • Durations are positive and bounded; poll_hot is at least 250 ms.
  • Allowed workspace roots must exist and must not escape via symlink.

Running: start, stop, status

herdr-phone start [--quick] [--foreground]   # start (and supervise) the daemon
herdr-phone stop                             # graceful shutdown via the control socket
herdr-phone status [--json]                  # mode, URL, and health
herdr-phone setup-link                       # rotate the pairing secret; print URL + QR
herdr-phone doctor                           # diagnose config, Herdr, and cloudflared
herdr-phone version
herdr-phone help

Invoke these from Herdr as plugin actions, or run the built binary directly when developing. As Herdr actions, output goes to the plugin log:

herdr plugin action invoke matheus3301.phone.start
herdr plugin action invoke matheus3301.phone.status
herdr plugin log list --plugin matheus3301.phone
  • start validates config, Herdr, cloudflared, and the state lock; spawns a detached herdr-phone serve; waits for private readiness; and prints an authenticated pairing URL. It is idempotent — if the daemon is already healthy it returns the current mode and URL, reconciling stale state via the control socket and process identity before replacing it.
  • stop requests graceful shutdown through the private control socket (it does not kill an arbitrary PID). New requests stop, WebSockets close, terminal controllers are released, cloudflared is asked to terminate within the grace period, and remaining process groups are killed.
  • status reports current mode, public URL, and the readiness of HTTP, Herdr, the tunnel, and the state engine to an authenticated caller or local status. --json emits machine-readable output.
  • doctor checks configuration and connectivity and prints exact Homebrew/manual guidance if cloudflared is missing. It never prints secrets.

The daemon does not start at login in v0.2.0 (an optional LaunchAgent is documented as future work but never generated silently).

Pairing and QR

Every daemon instance creates a fresh 256-bit single-use pairing secret. setup-link prints both a URL and a best-effort terminal QR code:

https://herdr.example.com/#pair=<base64url-secret>

The secret rides in the URL fragment (#pair=…), which browsers never send in an HTTP request. Open the link on your phone (scan the QR); the app removes the fragment from history and exchanges it for an HttpOnly, Secure, SameSite=Strict __Host- session cookie. The secret is single-use and rotates on success.

In named mode you must first pass Cloudflare Access (sign in as an allowed identity); pairing then binds the session, and the Access JWT is re-validated on every subsequent request and reconnect. In quick mode pairing is the only gate. To hand out a fresh link at any time, run herdr-phone setup-link again.

Installing the PWA on your phone

Herdr Phone is a Progressive Web App: display: standalone, maskable icons, and viewport-fit=cover. After pairing, add it to your home screen for a full-screen, app-like experience.

  • iOS (Safari): tap ShareAdd to Home Screen.
  • Android (Chrome): tap the menu → Install app (or Add to Home screen).

The app shell can be cached offline; API and terminal data are never cached. The app never trusts navigator.onLine — it revalidates on visibilitychange, pageshow, focus, online, freeze, and resume, and reconnects with jittered exponential backoff.

Feature guide

Everything is driven by explicit resource IDs and pane generations; reading the phone UI never relies on or silently changes Herdr UI focus.

  • Agents: the default route is an attention inbox. done is displayed as Updated, never as success; unknown remains separate from Idle.
  • Runs: a run binds an opaque ID to a pane generation and agent incarnation. Its detail view keeps exact workspace, worktree, tab, pane, and agent context next to the composer. Replacing the pane freezes the old run instead of silently rebinding it to the new occupant.
  • Instructions: accepted instructions enter the local runline. Rejected instructions remain editable. A timeout or disconnect becomes Delivery unknown with an explicit warning and choice, never an automatic retry.
  • Observed output: supported relays use GET /api/v1/runs and the generation-guarded run detail endpoint. Older relays fall back by capability to snapshot projection plus pane.read; fallback IDs are internal only.
  • Start run: choose the objective, execution location, and agent. Workspace, pane, agent, and prompt creation remain visible independent operations, so a later failure never hides or deletes earlier success.
  • Workspaces: inspect active runs and linked-worktree provenance first, then use the advanced view for tabs, panes, layout, agent startup, and destructive controls. Worktree removal is available only when Herdr identifies the workspace as a removable linked checkout.
  • Console: direct terminal control is an expert fallback rather than primary navigation. The real xterm.js controller preserves lifecycle generation across reconnects and names pane replacement instead of retrying forever.

Structural destructive actions use a confirmation dialog plus a single-use server nonce bound to the operation, resource id, lifecycle generation, and session. Terminal danger-pattern warnings are advisory and require a second tap — Herdr Phone never pretends to sandbox an authorized shell.

Deliberately not exposed to the browser: stopping the Herdr server, live handoff, plugin/integration administration, arbitrary socket methods, arbitrary process launch, and raw filesystem file reads.

Security model

Herdr Phone grants remote shell-equivalent access, so its defenses are layered and fail closed. In brief:

  • Loopback-only origin, reached from the internet only through an outbound-only Cloudflare Tunnel. Never bound to a LAN address in production.
  • Cloudflare Access in front of named tunnels, with the origin re-validating the Access JWT on every request and reconnect.
  • Mandatory pairing in every mode, with a single-use fragment secret and an HttpOnly __Host- session cookie; sessions live only in daemon memory.
  • One central middleware enforcing Host allowlist → Access JWT → session cookie → exact Origin allowlist → http.CrossOriginProtection + CSRF token → method, content-type, body-size, rate-limit, and deadline checks.
  • A strict CSP with only self-hosted assets, no unsafe-eval, no runtime CDN, no framing, and an explicit same-origin WebSocket allowance.
  • Terminal escape-sequence filtering (OSC 52/OSC 8, title set/report, DCS/APC/PM, device-status and answerback queries) before bytes reach the browser or any log.
  • No shell for subprocesses; exec.CommandContext, WaitDelay, and process groups tear down cloudflared and terminal controllers reliably.
  • No secret in argv, logs, the audit trail, browser storage, snapshots, or git. The audit trail records terminal input only as a byte count and category.

See SECURITY.md for the full model, the trust boundaries in docs/research/security.md, and immediate revocation steps for a compromised session, device, or tunnel token.

Architecture

cmd/herdr-phone ──> internal/app           # CLI dispatch and doctor
                    └──> internal/integration # production composition and teardown
                         ├──> internal/config   # strict TOML load + validation
                         ├──> internal/auth     # pairing, sessions, Access JWT/JWKS
                         ├──> internal/daemon   # lifecycle and runtime state
                         ├──> internal/herdr    # typed Herdr socket client and models
                         ├──> internal/state    # snapshot/run projections + generations
                         ├──> internal/server   # routes, middleware, run API, audit
                         ├──> internal/terminal # Herdr terminal-controller bridge
                         ├──> internal/tunnel   # cloudflared process and modes
                         ├──> internal/security # redaction and ANSI filtering
                         └──> internal/webui    # embedded production PWA
web/                # React + TypeScript + Vite + Tailwind v4 + xterm.js PWA,
                    # embedded into the Go binary (no runtime CDN)

The state engine polls session.snapshot (1.5 s while active, relaxing to 12 s when idle); Herdr events only trigger a debounced immediate poll. Snapshots are the source of truth, so a missed event costs one interval, never correctness. Per-pane lifecycle generations guard every mutation and terminal input against a pane that exited, closed, moved to a new ID, or changed occupant.

Development

The repository pins its toolchain with mise: the Go 1.26 series and Node.js 22.23.1. Run mise install once to match them, or prefix commands with mise exec --. CI and the release workflow pin the exact Go patch 1.26.5 (building from source requires Go 1.26.5+, because earlier 1.26 patch releases contain reachable standard-library vulnerabilities) and Node.js 22.23.1 (Vite 7 requires Node ≥ 22.12; a source build accepts Node 22.12+ on the 22 line or 24+).

make help          # list targets
make check         # fmt, tidy, vet, typecheck, lint, race tests, coverage,
                   #   frontend tests, embedded build, shell syntax, install smoke
make fmt           # format Go sources
make lint          # go vet + frontend lint
make typecheck     # TypeScript typecheck (tsc --noEmit)
make test          # go test ./...
make test-race     # go test -race ./...
make test-web      # frontend unit and component tests
make test-e2e      # Playwright mobile journeys (Chromium Pixel 7, WebKit iPhone 15)
make screenshots   # explicitly refresh tracked light/dark visual-review captures
make coverage      # coverage.txt with an enforced 80% Go threshold
make build-web     # locked frontend install + build into web/dist
make build         # build ./bin/herdr-phone with the frontend embedded
make verify-plugin # validate the manifest against Herdr in isolated state
make clean         # remove build, coverage, and frontend artifacts

make check runs every gate that needs no network or credentials. Tests are deterministic and require no real Cloudflare account, Access identity, tunnel token, Herdr session, or browser: Go tests use fakes (Herdr, a fake cloudflared on PATH, generated JWKS, injected clocks) and the frontend uses component tests plus Playwright on mobile viewports.

make verify-plugin links the plugin into a throwaway Herdr state directory and confirms it and its global actions are discoverable, without touching your active Herdr session. By default it downloads the pinned official Herdr v0.7.5 (verified SHA-256) for reproducibility; set HERDR_BIN=/path/to/herdr to use a specific binary, or HERDR_USE_PATH=1 to use a herdr from your PATH.

The install script (scripts/build.sh) builds from source when compatible Go and Node toolchains are present and otherwise downloads a checksum-verified release archive; scripts/smoke-install.sh exercises that download-and-verify path offline with locally generated fake assets.

Releasing

Releases are cut by pushing an annotated (or signed) vX.Y.Z tag whose version matches herdr-plugin.toml and the binary's build info, on a commit that is already on main:

git tag -a v0.2.0 -m "herdr-phone v0.2.0"
git push origin v0.2.0

The release workflow requires an annotated/signed tag whose commit is on main, verifies the version match, runs the full quality gates, plugin verification, and govulncheck (source and built binaries), and uses GoReleaser to publish macOS amd64/arm64 .tar.gz archives, checksums.txt, and an SBOM. It then records a keyless GitHub build-provenance attestation over the archives and checksums.txt (see Verifying a downloaded release). CI never bumps versions or pushes commits.

Supply-chain hygiene: every GitHub Action is pinned to a full commit SHA (with a version comment, kept current by Dependabot), govulncheck is pinned to a released version, workflows default to contents: read with write scope only on the publish job, and forks never receive secrets (pull_request, not pull_request_target).

Troubleshooting

  • herdr plugin is missing / plugin commands not found. Some Homebrew herdr 0.7.5 bottles report the right version but omit the plugin command. Verify with herdr plugin. If absent, install the official Herdr release binary. This plugin never modifies your Herdr installation automatically.
  • cloudflared not found. It is never auto-installed. Run herdr-phone doctor for exact guidance, or brew install cloudflared.
  • Named mode refuses to start. It requires an HTTPS public_url, auth.access.enabled = true with team_domain and audience, and exactly one tunnel credential strategy. doctor reports which requirement is unmet.
  • 403 / Access denied in the browser. Your Cloudflare Access policy must allow your identity, and allowed_identities (if set) must list your exact email. Access is deny-by-default.
  • Paired but requests fail after a while (named mode). Sessions expire at the earlier of session_ttl and the Access JWT expiry. Re-authenticate through Access; the origin re-validates the JWT on every request.
  • Quick Tunnel won't start. Set both quick_enabled = true and start with --quick. Remember a Quick Tunnel has no edge identity and is for testing only.
  • Terminal shows a conflict when opening a pane. Only one controller owns input at a time. Confirm the explicit takeover to seize it.
  • The public URL doesn't reach this instance (quick mode). Herdr Phone verifies the public URL against a one-time instance probe before printing the pairing link; a mismatch means a stale or foreign tunnel — stop and restart.
  • The pairing link expired. The secret is single-use. Run herdr-phone setup-link for a fresh one.
  • Something is compromised. Follow the revocation steps in SECURITY.md: stop the daemon, revoke Access sessions, and rotate the tunnel token.

Non-goals (v0.2.0)

No Windows host support; no native iOS/Android apps, APNs, or background push actions; no multi-user collaboration or simultaneous terminal controllers; no automatic Cloudflare tunnel/DNS/Access provisioning; no automatic cloudflared installation or self-update; no start-at-login or reboot survival without user configuration; no multi-session Herdr aggregation; no parsing of agent-specific approval screens into native controls; and no file browsing beyond directory selection, file upload, clipboard image transfer, or arbitrary downloads. See SPEC.md §21 for the full list.

Contributing

Contributions are welcome — see CONTRIBUTING.md and the Code of Conduct. Because this tool grants remote shell-equivalent access, every change is reviewed for its effect on the security posture.

License

MIT © Matheus Monteiro and contributors.

Directories

Path Synopsis
cmd
herdr-phone command
Command herdr-phone is a Herdr plugin that relays one authenticated operator's Herdr session to their phone over a Cloudflare tunnel.
Command herdr-phone is a Herdr plugin that relays one authenticated operator's Herdr session to their phone over a Cloudflare tunnel.
internal
app
Package app wires configuration, the version source, and the orchestration backend into the herdr-phone command-line surface.
Package app wires configuration, the version source, and the orchestration backend into the herdr-phone command-line surface.
auth
Package auth implements the origin-side authentication for herdr-phone: Cloudflare Access JWT validation (RS256 with JWKS), single-use pairing, the in-memory HttpOnly session lifecycle, and CSRF tokens.
Package auth implements the origin-side authentication for herdr-phone: Cloudflare Access JWT validation (RS256 with JWKS), single-use pairing, the in-memory HttpOnly session lifecycle, and CSRF tokens.
buildinfo
Package buildinfo is the single source of truth for herdr-phone identity and version.
Package buildinfo is the single source of truth for herdr-phone identity and version.
config
Package config loads and strictly validates herdr-phone configuration.
Package config loads and strictly validates herdr-phone configuration.
daemon
Package daemon provides the long-lived herdr-phone daemon primitives: a private mode-0600 Unix control socket, secret-free runtime state, start-time reconciliation against an existing instance, generic child supervision behind small interfaces, status aggregation, a pairing-rotation hook, and graceful stop.
Package daemon provides the long-lived herdr-phone daemon primitives: a private mode-0600 Unix control socket, secret-free runtime state, start-time reconciliation against an existing instance, generic child supervision behind small interfaces, status aggregation, a pairing-rotation hook, and graceful stop.
herdr
Package herdr is the single typed owner of the Herdr socket wire protocol (protocol 17, schema version 1, Herdr 0.7.5).
Package herdr is the single typed owner of the Herdr socket wire protocol (protocol 17, schema version 1, Herdr 0.7.5).
integration
Package integration composes the herdr-phone subsystems — configuration, the Herdr socket client and state engine, Cloudflare Access authentication, pairing and sessions, the security middleware and HTTP/WebSocket server, the terminal controller bridge, the cloudflared supervisor, and the daemon control socket — into the concrete app.Runtime the CLI drives.
Package integration composes the herdr-phone subsystems — configuration, the Herdr socket client and state engine, Cloudflare Access authentication, pairing and sessions, the security middleware and HTTP/WebSocket server, the terminal controller bridge, the cloudflared supervisor, and the daemon control socket — into the concrete app.Runtime the CLI drives.
security
Package security implements the origin-side defensive controls for the herdr-phone relay that are shared across packages: secret and control-character redaction for log/audit sinks, and a streaming ANSI terminal filter for controller output.
Package security implements the origin-side defensive controls for the herdr-phone relay that are shared across packages: secret and control-character redaction for log/audit sinks, and a streaming ANSI terminal filter for controller output.
server
Package server implements the versioned loopback HTTP and WebSocket relay for herdr-phone.
Package server implements the versioned loopback HTTP and WebSocket relay for herdr-phone.
state
Package state is the poll-as-truth Herdr state engine.
Package state is the poll-as-truth Herdr state engine.
terminal
Package terminal bridges a browser WebSocket to a single `herdr terminal session control` subprocess.
Package terminal bridges a browser WebSocket to a single `herdr terminal session control` subprocess.
tunnel
Package tunnel manages the cloudflared child process that fronts the loopback relay origin.
Package tunnel manages the cloudflared child process that fronts the loopback relay origin.
webui
Package webui embeds the built React/TypeScript PWA and serves it as a single-page application from the Go binary.
Package webui embeds the built React/TypeScript PWA and serves it as a single-page application from the Go binary.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL