Remote phone access to the DSH Web UI: scan a QR code for LAN or public (cloudflared tunnel) access with real-time sync, a mobile-adaptive layout, and a settings tab.
Install
# from npm (prebuilt)
dsh plugin --profile web add dsh-pocket
# from GitHub (first run asks for allowBuilds approval — follow the hint, retry)
dsh plugin --profile web add github:shaobeichen/dsh-pocket
Any plugin you install runs third-party code with your own permissions — it can read your files, use your credentials, and reach the network, and tool approvals don’t sandbox it. GitHub-sourced plugins also run build scripts at install time — pnpm blocks those until you allow them, so an install can stop with ERR_PNPM_GIT_DEP_PREPARE_NOT_ALLOWED or ERR_PNPM_IGNORED_BUILDS; dsh prints the exact key to add under allowBuilds in your profile’s pnpm-workspace.yaml, and the install works on the next run. Allowing a build is a trust decision: only install sources you trust, and pin a commit (github:owner/repo#sha).
README
Put DeepSeek Harness in your pocket: one package, one settings tab — scan a QR code and your phone shows exactly what's on your computer screen, live, from anywhere.
What is this
You want to use DeepSeek Harness on your computer, even when you're not at the computer.
- On your way home, the agent is running a task on your computer — pull out your phone and see where it is, what it produced.
- Out and about, you want the agent on your computer to look something up or write a snippet — no remote desktop, no SSH.
- The computer is at home or in the office, you're elsewhere, and you want to drive your DeepSeek Harness from your phone — send tasks, watch the output, tap approvals.
That's what DSH Pocket does: install it, scan a QR code, and your phone shows and controls the DeepSeek Harness UI in real time — from anywhere.
What it looks like — the phone shows the exact same UI as your computer, live:
✨ Features
| Feature | Description |
|---|---|
| 📶 LAN QR access | Works out of the box: Settings → Phone access — scan the LAN QR on the same Wi-Fi (auto-detects the LAN IP; under WSL it picks the Windows host's physical NIC IP) |
| 🚪 LAN switch | Turn LAN access off/on with one click in Settings (a confirmation dialog shows each time): off kills the LAN QR code and link instantly; public access is unaffected |
| 🌐 Public QR (from anywhere) | Click "Enable anywhere" → cloudflared tunnel → scan the public QR over 4G / any network |
| 🏷️ Fixed public hostname | Optional "Named tunnel" mode: paste a Cloudflare Tunnel Token + your own domain — the public address stays fixed across restarts (see below) |
| 🔐 Access PIN | Public links use an 8-digit random PIN by default (rotated on every tunnel start; customizable to a fixed PIN — custom PINs are not rotated); LAN has its own separate 8-digit random PIN (on by default; switchable off in Settings — then LAN scans connect directly) |
| 🔑 Custom PINs | Both the public and LAN PINs can be set to a fixed 8–64-character PIN using letters and digits in Settings (custom PINs are never auto-rotated) |
| 🧘 Session persistence | Enter the PIN once and you're set for a long time (login is tied to the computer's dsh web process: as long as it stays up, the phone won't ask again; after a dsh web restart/update, enter it once more) |
| ⚡ Real-time sync | Streaming output passes through WebSocket untouched — what the computer renders, the phone renders live; fully interactive both ways; built-in WS heartbeat keep-alive (defeats silent NAT/battery link drops with auto-reconnect) |
| 📱 Mobile-adaptive layout | Narrow screens get a drawer layout automatically (ported from dsh-web-mobile, MIT): sidebar drawer, full-width conversation, safe-area insets, touch optimizations |
| 🧭 Optional right sidebar | Shows the native right-sidebar entry on mobile; disable it for a compact phone header or keep it available alongside the terminal dock on an unfolded display |
| 📁 File browser | The mobile "Files" entries need a host-side explorer panel (a dsh-web-ui component); on stock DSH without it the entries are auto-hidden instead of doing nothing |
| 🗜️ Transfer compression | Large JSON responses are gzip/brotli'd on the fly (17MB session history → ~1MB; brotli quality 6: fast and bandwidth-friendly) — faster loads, less mobile data |
| 🔁 Tunnel auto-restore | After a DSH restart the previously-running public tunnel comes back automatically |
| 🧩 Zero-dependency install | One npm package, one settings tab — no core/adapter split, no account, no server |
🚀 Usage
Where the entry is: after installing and restarting dsh web, open Settings — the left sidebar shows "Phone access" at the top level (same level as General / Models):
Prerequisite: DeepSeek Harness installed. If your terminal says dsh: command not found, install it first:
npm install -g @deepseek-ai/dsh # global install; verify: dsh --version
# No global install? Prefix every command with: npx @deepseek-ai/dsh
# 1. Install the plugin (everything in one package)
dsh plugin --profile web add dsh-pocket -w
# 2. Restart dsh web
npx @deepseek-ai/dsh web
LAN (same Wi-Fi)
Settings → Phone access → scan the "📶 LAN" QR code → enter the LAN PIN (shown in the LAN block; hit Refresh to roll a new one, or Customize to set an 8–64-character alphanumeric PIN) → the phone opens the exact same DSH, in real time.
The "LAN access" switch is on by default and can be turned off/on with one click (a confirmation dialog shows each time). Off kills the LAN QR code and link instantly (phones can't open them); public access is unaffected. Tap "On" to restore it.
The LAN PIN is on by default (security-first). If you're the only user and find typing it every time annoying, flip "LAN access PIN" to Off in the LAN block — LAN scans then connect directly with no PIN (LAN-only devices; the public tunnel always requires a PIN, unaffected).
After logging in once, the phone won't ask again: as long as the computer's dsh web keeps running, reopening the phone needs no PIN (a dsh web restart/update asks for it once more).
Advanced option: auto-detection may not pick a reachable address for Tailscale/VPN setups. You can select a detected IP from the "LAN address" dropdown; normally no change is needed.
Public (from anywhere)
On the same page click "Enable anywhere" → a security disclaimer pops up every time — check "I understand and agree" to proceed (on a corporate/classified network, confirm compliance first) → wait for the tunnel (first run downloads cloudflared; macOS/Linux use the Tsinghua mirror, seconds) → scan the "🌐 Public" QR code → the phone opens the link and enters the access PIN (shown in the settings page's public section; the default is an 8-digit random PIN rotated on every tunnel start, or use Customize for a fixed 8–64-character alphanumeric PIN that is never rotated) → works from outside (4G / office network).
Upgrading:
dsh plugin --profile web update dsh-pocket --latest -w(--latestis required across major versions — a^0.xrange won't auto-jump to 1.x).
Fixed public hostname (named tunnel, optional)
The default "quick tunnel" gets a new random URL on every restart. For a fixed public address, use a Cloudflare named tunnel (requires a Cloudflare account + your own domain):
- In Cloudflare Zero Trust → Networks → Tunnels, create a Tunnel and copy the Tunnel Token
- In that Tunnel's Public Hostname, point your domain (e.g.
pocket.example.com) tohttp://127.0.0.1:3081 - Back in the settings page's public section: switch the mode to "Named tunnel", paste the Tunnel Token, enter the fixed hostname, and save
- Click "Enable anywhere" → the public address is now your own hostname and survives restarts
Note: in named-tunnel mode the public PIN is not auto-rotated (the address is fixed, so the PIN stays the same across restarts) — manage it proactively with a custom PIN. The Tunnel Token is stored locally only ($DSH_HOME/dsh-pocket/settings.json, readable by your user only) and is never echoed back in the UI.
⚠️ Security (read first)
- DSH can execute code on your computer. LAN QR/URL plus its own 8-character PIN is the key (PIN on by default, switchable off — then LAN scans connect directly, same-network devices only) — never share the LAN QR, URL or PIN.
- Read and accept the security disclaimer before enabling public access (the dialog shows on every enable; the server enforces it, so it can't be bypassed): public = exposing a code-executing DSH to the internet — use a strong PIN, turn it off when done, never on classified networks.
- Public access uses an 8-digit random PIN by default: the link is random, the PIN rotates on every tunnel start, and old links die instantly — even a leaked link can't get in. A custom PIN may contain 8–64 letters and digits and is never auto-rotated.
- Phone login state is tied to the computer's dsh web process: no re-entry while dsh web stays up; one re-entry after a restart/update.
- Login rate limiting (anti brute-force): 5 consecutive wrong PINs from the same IP lock it for 60s; a global failure threshold briefly locks everyone (blocks distributed IP-rotation scans); a successful login resets the counter.
- The public URL is randomly assigned by cloudflared and changes on every restart (old links die automatically — a natural key rotation); in named-tunnel fixed-hostname mode the address stays and the PIN is not auto-rotated — manage it with a custom PIN.
- Public detection is fail-closed (issue #66): everything except loopback and private LAN addresses is treated as public and PIN-gated — including any self-hosted tunnel / reverse proxy pointing at the local port with its own hostname. There is no "change the domain to bypass the PIN" hole.
- LAN mode exposes nothing publicly; only devices on the same network can reach it.
- Built for personal use; the public PIN lives in
$DSH_HOME/dsh-pocket/token(re-rolled per tunnel start unless customized), the LAN PIN in$DSH_HOME/dsh-pocket/token-lan(refreshed manually in Settings), and switches/custom flags in$DSH_HOME/dsh-pocket/settings.json.
💻 DSH Desktop
- In the desktop app, QR screen-mirroring works; update/restart are managed by the desktop app (auto-disabled here).
- ⚠️ The desktop advanced mode doesn't support phone access yet (it disables the web layout; the phone gets no layout service → blank screen). Switch back to compatibility mode and restart; phones opening an advanced-mode page will see a clear notice overlay.
🩹 Troubleshooting (traps users step on)
| Symptom | Cause & fix |
|---|---|
dsh: command not found / "DSH is not defined" |
dsh CLI missing: npm install -g @deepseek-ai/dsh, or prefix commands with npx @deepseek-ai/dsh |
ERR_PNPM_ADDING_TO_ROOT |
pnpm 9 workspace-root restriction: append -w (--workspace-root) to install/update commands |
| Nothing changed after install/update | You must restart dsh web; the running process still loads the old code |
listen EADDRINUSE ... :3081 |
A stale dsh-pocket process holds the port: macOS/Linux lsof -ti :3081 | xargs kill -9; Windows netstat -ano | findstr :3081 (find the LISTENING PID) → taskkill /PID <PID> /F, then retry |
| Want a different port (issue #70) | Plugin mode: write "proxyPort": 3082 into $DSH_HOME/dsh-pocket/settings.json and restart dsh web. CLI mode: dsh-pocket --port 3082. If the port is taken you'll get EADDRINUSE — kill the old process or pick another one |
| Issue a temporary PIN to a guest | Not available: the temporary access PIN feature (issue #69) was removed in 2.6.x (it crashed on revoke). To share access, send the main PIN or a ?token=<main PIN> link, then hit "Refresh" in Settings once the guest is done |
| cloudflared install fails on a remote Linux server (issue #45) | If all CDN sources (GitHub / ghproxy / gh.ddlc / gh-proxy) are unreachable on a remote Linux host, install cloudflared yourself (e.g. apt install cloudflared, dnf install cloudflared, or download the tgz and unpack it), then add "cloudflaredPath": "/path/to/cloudflared" into $DSH_HOME/dsh-pocket/settings.json and restart dsh web. The plugin will then use that binary directly and skip the auto-download |
| Version stuck below 1.x | ^0.x ranges never jump to 1.x: update with --latest (dsh plugin --profile web update dsh-pocket --latest -w) |
Public error 1033 |
See "Public tunnel troubleshooting" below — usually a local proxy/VPN (Clash etc. TUN mode) killing the tunnel |
| After "Restart dsh web", the page says the process is running in the background | The new process from in-page self-restart is a detached background process (not attached to your terminal) — that's the standard way to apply updates in-page; stop it: macOS/Linux lsof -ti :3080 | xargs kill -9; Windows netstat -ano | findstr :3080 → taskkill /PID <PID> /F (logs under $DSH_HOME as dsh-pocket-restart-*.log) |
⚠️ Public tunnel troubleshooting (read first)
Symptom: after clicking "Enable anywhere", the public URL shows error 1033 (Tunnel error) on the phone.
Most common cause: a local proxy/VPN (Clash, Surge, v2ray, sing-box, etc., especially in TUN mode).
Such tools take over all traffic and often cut cloudflared's tunnel-edge connections
(*.argotunnel.com, Cloudflare edge IPs), so the tunnel registers but the data plane never connects.
Fix (try in order, lightest first):
- First just turn off the proxy's TUN mode — no need to quit the proxy; this is enough in most cases:
- Clash: turn off the "TUN mode" toggle in Settings (or right-click the menu-bar icon → uncheck TUN mode)
- Surge: turn off "Enhanced mode"; v2ray/sing-box: turn off "virtual NIC / route takeover"
- Then go back to the settings page and click "Enable anywhere" again
- If that's not enough, temporarily fully quit the proxy (not just close the window: quit Clash from the menu-bar icon; if a
background service is installed, stop it in the service manager and confirm with
ps aux | grep clash), then retry. - Add DIRECT rules to the proxy for the tunnel domains and Cloudflare edge (Clash example):
- DOMAIN-SUFFIX,argotunnel.com,DIRECT - DOMAIN-SUFFIX,trycloudflare.com,DIRECT - IP-CIDR,198.41.192.0/24,DIRECT,no-resolve - If the network really can't reach the tunnel, use LAN mode: turn on the phone hotspot → connect the computer to it → scan the LAN QR. Same experience, from anywhere.
Other causes: corporate firewalls / campus networks blocking outbound — ask IT to allow it, or use a hotspot.
First run: "Downloading cloudflared" fails or hangs:
- macOS/Linux: the plugin first downloads from the Tsinghua mirror (measured ~3MB/s, done in seconds); falls back to official GitHub + acceleration mirrors if it fails.
- Windows: no Tsinghua mirror (Homebrew doesn't support Windows) — downloads the ~50MB exe from GitHub directly; single-threaded, so it's slower — that's expected, wait a few minutes, or use a proxy.
- If all sources fail, the settings page shows a hint. Alternatives (any one):
- Install the
cloudflaredcommand and retry (the plugin then uses the PATH binary, no download):- macOS:
brew install cloudflared; Linux:sudo apt install cloudflaredor from the official site - Windows:
winget install cloudflaredor from the official site - Any platform:
npm i -g cloudflared
- macOS:
- Enable a proxy (system proxy / Clash etc.) and click "Enable anywhere" again
- Manually download the binary into
$DSH_HOME/dsh-pocket/bin/($DSH_HOMEis usually~/.dsh, on Windows%USERPROFILE%\.dsh; name itcloudflared(add.exeon Windows) or the release asset name — both are recognized)
🗂 Architecture (single package)
| File | Purpose |
|---|---|
lib/index.js |
Plugin entry: auto-start proxy + register RPC + access-PIN management (public: 8 digits rotated per tunnel start; LAN: separate 8 digits, manually refreshable / switchable) + LAN access switch + DSH Desktop detection |
lib/settings.mjs |
Settings persistence: LAN access switch (on by default) + LAN-PIN switch (on by default) stored in $DSH_HOME/dsh-pocket/settings.json |
lib/service.mjs |
Service: proxy lifecycle (port auto-fallback), public tunnel (auto-restore), status snapshot (with QR data URLs) |
lib/proxy.mjs |
Header-rewriting reverse proxy: Host/Origin → loopback, HTTP + WebSocket passthrough + polyfill injection + gzip/brotli compression + per-host token auth (public always; LAN per switch) + blocks LAN Hosts when LAN is off |
lib/tunnel.mjs |
cloudflared: multi-mirror download (Tsinghua first) / adaptive parallel / start / parse public URL (HTTP/2) |
lib/web-rpc.js |
Loopback RPC: status / tunnel.start / tunnel.stop / lan.setEnabled / version / update / restart |
client/ |
"Phone access" settings tab + mobile adaptation (dsh-web-mobile port) |
bin/dsh-pocket.mjs |
CLI: LAN/public modes, prints URL + QR |
🛠 Development
npm install
node client/build.mjs # rebuild after editing client/
npm test # proxy / auth / compression / tunnel / service / RPC / settings (109 tests)
Want to try your changes locally without publishing? Point the installed plugin at your local checkout with a symlink and restart dsh web. Full steps (including switching back to the npm release) are in LOCAL-DEV.md.
🤝 Credits
- Mobile adaptation ported from mexiaosqwq/dsh-web-mobile (MIT)
- Public tunnel powered by cloudflared
📄 License
GPL-2.0 — copyleft: free to use, modify, and redistribute, but derivatives must stay GPL and keep the copyright notice; commercial use included.
Note: the mobile-adaptation portion is ported from dsh-web-mobile (MIT, GPL-compatible); its copyright notice stays in
client/mobile/LICENSE.dsh-web-mobile.
Questions? Feedback welcome: bugs, ideas, or feature requests — open an issue at GitHub Issues 🙏
Links
More in this category
xmanrui/dsh-im★ 1525
Connect IM bots to DeepSeek Harness via QR codes or bot credentials (9 channels: Feishu, WeChat, DingTalk, WeCom, QQ, Slack, Telegram, Discord, and WhatsApp).
inclusionAI/Avernet#deepseek-harness-channel-bcn★ 572
Connects DeepSeek Harness to Avernet's Bot Collaboration Network over WebSocket V2, with automatic onboarding, isolated agent sessions, tool-call events, and multi-bot routing tools.
omdsh-dev/dsh-notification★ 85
Desktop notifications for turn completions, with per-outcome controls and keyword rules.
whyihaveyou/dsh-suite#plugin-notify★ 57
IM webhook and local notifications on turn completion, errors, or approval (Feishu/WeCom/DingTalk/Slack/Discord/custom).
omdsh-dev/dsh-lark★ 55
Lark/Feishu bot channel for DeepSeek Harness: each chat drives its own agent, and tool approvals, model questions, and plan reviews return as cards answered by a button or a reply. Switch workspace and model from the chat (`/cd`, `/model`, `/new`), and run several bots that keep separate sessions and can hand turns to each other in one group.
THEWOLFWALKER/dsh-notifier★ 54
DSH notification & remote-control plane: one `notify()` API across 28 outbound channels, six inbound control channels, phone approvals/questions/task control, a Native DSH Notify & Control UI, live outbound hot-apply, multi-agent routing, and zero runtime dependencies.
Community comments
Comments are public GitHub Discussions. Loading them connects to GitHub and Giscus; a GitHub account is required to post.