Operations & Maintenance

August 15, 2026 · View on GitHub

Everything that happens after the first pairing: CLI reference, updates, backups, a permanent URL, troubleshooting, and how plugins behave under remote access.

CLI reference

On Linux and macOS the command is dsh-remote (use sudo where noted); on Windows it is dsh-remote.ps1 (run from an elevated PowerShell). All three platforms expose the same operations; the table below shows the Linux/macOS spelling.

Connection & pairing

CommandWhat it does
sudo dsh-remote pair [--label NAME] [--qr]Generate a pairing code (name the device; prints a QR when qrencode is installed)
dsh-remote devicesList paired devices
sudo dsh-remote devices revoke <id|name|all>Revoke one device or all of them
sudo dsh-remote revokeRevoke every session (same as devices revoke all)
dsh-remote url [--qr]Show the current tunnel URL (auto-refreshes from the tunnel log)

Pairing works two ways — a camera is never required:

  • Scan — the phone camera scans the QR printed by pair (Linux/macOS print it in the terminal; the Windows CLI draws its own QR).
  • Copy & paste — send the invite link (or the 8-character code) to the new device: opening the link in any browser pairs it instantly; or type the code on the pairing page. The Devices page (/devices) has a one-click Copy invite link button, so you can share the link through any messaging app.

Status & diagnostics

CommandWhat it does
dsh-remote statusOverall status (services + tunnel + health)
dsh-remote healthFour-way check: DSH + gateway + URL + pairing path
sudo dsh-remote doctorFull health check: versions / services / ports / URL / disk / audit
dsh-remote logs [-n N]DSH logs
dsh-remote logs-tunnelcloudflared logs

Notifications

CommandWhat it does
sudo dsh-remote notify-config set --bark <URL> [--serverchan <KEY>] [--telegram-bot <T> --telegram-chat <ID>] [--webhook <URL>]Configure push channels
sudo dsh-remote notify-config showShow the current config (masked)
sudo dsh-remote notify test [channel]Send a test notification (bark|serverchan|telegram|webhook; all by default)

The companion plugin pushes to your phone when a task completes or gets blocked.

Updates & services

CommandWhat it does
sudo dsh-remote update --checkShow the version diff only
sudo dsh-remote updateUpgrade DSH: auto backup → install latest → record previous → restart
sudo dsh-remote update --version <v>Upgrade to a specific version (backs up first)
sudo dsh-remote rollbackRoll back to the pre-upgrade version
sudo dsh-remote backup / backup --restore <file>Backup/restore sessions + config + keys (keeps the latest 5)
sudo dsh-remote update-cloudflaredUpgrade cloudflared and restart the tunnel
sudo dsh-remote self-update <new install.sh>Upgrade this suite (backup first, validate the script, then run)
dsh-remote kit-versionSuite + DSH + Node + cloudflared + gateway versions
sudo dsh-remote dsh-reloadRestart only the DSH core and health-check it (use after installing a new plugin)
sudo dsh-remote restart / start / stopRestart / start / stop the whole stack

⚠️ Prefer dsh-reload over restart when you only need to pick up plugin changes: restart also restarts the tunnel, and a quick tunnel gets a new URL on every restart, breaking saved bookmarks. (dsh-reload never touches the tunnel.)

Automation (all platforms)

  • Supervision — every daemon auto-restarts on crash and at boot: systemd on Linux, launchd KeepAlive on macOS, Task Scheduler with restart settings on Windows.
  • Watchdog — a 5-minute heartbeat checks DSH and the gateway independently and restarts the failing one, with a 5-minute anti-restart-storm guard.
  • Log rotation — cloudflared logs daily (7 kept) and the audit log weekly (12 kept), rotated with copytruncate so nothing needs to restart (Linux; macOS/Windows rely on the same copytruncate-friendly append-only files).

A quick tunnel gets a new URL whenever cloudflared restarts (run pair again to recover). For a set-and-forget URL, use a Cloudflare named tunnel — a cheap domain plus a free Cloudflare account:

  1. Create a tunnel in the Cloudflare dashboard and copy its token.
  2. Linux: sudo bash install.sh --tunnel-token <token> --public-url https://dsh.your-domain.com (macOS/Windows installers accept the same two options).
  3. Optionally add a Cloudflare Access policy (email verification) as one more gate.

Troubleshooting

SymptomFix
Phone scan won't open the pageOn the host, dsh-remote health localizes it with the four-way check
Pairing code invalidExpired or used: sudo dsh-remote pair again
Broken after a host rebootThe quick-tunnel URL changed: sudo dsh-remote pair (named tunnels are unaffected)
Suddenly back at the pairing pageSession revoked or expired: pair again
Broken after an upgradesudo dsh-remote rollback
Blank web pagedsh-remote logs; sudo dsh-remote dsh-reload
Not sure about anythingsudo dsh-remote doctor and fix the ✗ items
Slow/unstable from mainland ChinaThe tunnel relays through Cloudflare; see "Self-hosted relay" below

Plugins & remote access

Plugins installed on the server work for remote devices. DSH plugins come in two planes:

Plugin kindRuns on serverUsable remotelyWhen it takes effect
Host plane (events / tools / HTTP routes, no UI)✅ automatic — independent of who connects (e.g. the bundled notification plugin pushes to your phone)Add a row to $DSH_HOME/profiles/web/cordis.patch.yml — hot-applied (the file is HMR-watched, no restart)
Client UI plugin (package declares dsh.client)✅ (builds a client bundle)✅ the gateway transparently proxies /plugins/* and the __DSH_BOOT__ injection — the remote UI is identical to localThe plugin set is scanned at startup: after dsh plugin --profile web add <pkg>, restart the DSH core; browsers then see the new UI on refresh
Plugin opening its own port (bundled MCP / admin UI)❌ not behind the gateway — unreachable remotely

Installing a new client UI plugin (server-side, verify on the phone)

# 1. Install (on the server; as root, make sure DSH_HOME points to the install dir)
sudo dsh plugin --profile web add <plugin-package>
# 2. Restart only the DSH core (the plugin set is scanned at startup; health-checks after)
sudo dsh-remote dsh-reload
#    ⚠️ Do NOT use `dsh-remote restart`: it also restarts the tunnel, which changes a
#       quick-tunnel URL and breaks the phone's bookmark
# 3. Confirm the plugin loaded
sudo dsh-remote logs | grep -i <plugin>

Then refresh the page on the phone and the new plugin is there (Settings → Plugins). No re-pairing is needed: the pairing session lives on the gateway side, and restarting the DSH core doesn't touch it. The gateway answers 502 for a few seconds during the restart — refresh after health passes.

FAQ

  • Is dsh-remote itself a DSH plugin? No. dsh-remote is a standalone companion app installed next to DSH (gateway + tunnel + watchdog + CLI) — it is not loaded into DSH's profile tree. It ships exactly one DSH plugin, dsh-remote-plugin, which is installed with the official dsh plugin --profile web add flow and handles task notifications. The repository still carries the dsh-plugin topic because it lives in the DSH ecosystem (like the other tunnel/auth companions on the awesome-dsh-plugin list).
  • Is a plugin usable remotely right after install? Host plane: appending the registration row to cordis.patch.yml hot-applies it. Client UI plugins: install → dsh-reload → refresh the remote browser. There is no update-without-refresh in production (that is a dev-mode client-HMR capability).
  • Can the remote UI differ from local? No. The UI is injected same-origin by the server, and the gateway forwards HTTP and WebSocket transparently; plugin bundle URLs carry content hashes, so a refresh always gets the latest.
  • Can I install plugins remotely? No. Settings → Plugins is a read-only inventory; installation happens on the server CLI only.
  • Gotchas — a plugin whose client code hardcodes http://localhost:3080 absolute URLs breaks remotely (well-behaved plugins use relative paths); "open local file / native command" buttons are hidden or degraded remotely (see SECURITY.md).

Self-hosted relay (advanced)

Cloudflare's edge can technically see decrypted traffic — that is true of every public tunnel service. For sensitive code, replicate Paseo's relay architecture: rent a small VPS (~$5/month), run frps as a TCP forwarder, and forward the gateway port 127.0.0.1:8080 out. Gateway, pairing, and watchdog are all reused; only the transport layer is swapped. See also getpaseo/paseo-relay for the original.

Directory layout

dsh-remote/
├── README.md / README.zh-CN.md   Homepage (English / 简体中文)
├── LICENSE · SECURITY.md · CONTRIBUTING.md · CODE_OF_CONDUCT.md
├── .github/                      Issue / PR templates and CI
├── server/                       Linux host (full edition)
│   ├── install.sh                One-command deployment (embeds every component)
│   ├── gateway.js                Security gateway (pairing / devices / audit / proxy / PWA)
│   ├── dsh-remote                Ops CLI (Linux)
│   ├── watchdog.sh               Heartbeat watchdog (Linux)
│   └── dsh-web.service etc.      systemd units
├── host/
│   ├── macos/install-macos.sh    macOS host (full edition, launchd)
│   │   ├── dsh-remote.sh         Ops CLI (macOS)
│   │   └── dsh-watch.sh          Heartbeat watchdog (macOS)
│   └── windows/
│       ├── install-windows.ps1   Windows host (full edition, Task Scheduler)
│       ├── dsh-remote.ps1        Ops CLI (Windows, with QR codes)
│       └── dsh-watch.ps1         Heartbeat watchdog (Windows)
├── plugin/                       Companion notification plugin (dsh-remote-plugin)
├── tools/inject.ps1              Authoritative sources → embedded copies in install.sh
├── docs/
│   ├── operations.md             This document
│   └── migration/                Migration guides from other agent tools
└── client/                       Per-platform client connection guides