deepseek-harness-lan
Identified★ 6Run DeepSeek Harness Web UI on your home LAN — 4 minimal patches + one-click apply/revert scripts | 让 DeepSeek Harness 的 Web UI 在局域网跑起来
🐾 deepseek-harness-lan
English | 简体中文
Run DeepSeek Harness Web UI on your home LAN — bind a specific IP, trust it, and open it from any device.
🎯 Target version: dsh
0.1.2-rc.1(commita66e470204) — the patch is written and verified against this version; other versions may break (see 🧩 Compatibility).
📑 Table of Contents
- 📖 What is this
- ✨ Features
- 🚀 Quick Start
- 🔀 Alternative: no source changes (SSH tunnel)
- 🧠 How it works
- 🔒 Security notes (please read)
- 🧩 Compatibility
- 🗺️ Version compatibility matrix
- ❓ FAQ
- 🧩 Patch files
- 🤝 Contributing
- 🎉 Acknowledgements
- 📜 License
📖 What is this
dsh's Web UI is officially restricted to localhost access only. --host 0.0.0.0 fails immediately:
error: --host 0.0.0.0 is intentionally not supported yet for safety:
it would expose remote code execution to the network; use 127.0.0.1 instead
This is a deliberate security design — the dsh Web UI sits on top of an agent that can execute arbitrary commands, and the official project does not want it exposed to the network.
But what if you want to open dsh from Windows, phones, or tablets on your home/office LAN? This repository provides 3 minimal patches + one-click scripts that let you bind a specific LAN IP and run the dsh Web UI safely.
✨ Features
- 🔧 3 minimal diffs — only the necessary source changes, no extra dependencies (two of the original four LAN changes were adopted upstream by 0.1.2-rc.1:
--trusted-hostCLI +trustedHostsfence inrpc-host.ts, so those files are no longer patched) - 📦 One-click apply / revert (
apply.sh/revert.sh) — auto-detection, duplicate-application guard - 🧩 Does not break the official security model: the
0.0.0.0wildcard is still rejected; the--trusted-hostexplicit trust mechanism is preserved - 🧪 Verified on dsh 0.1.2-rc.1: full
pnpm run build, page load, API calls, and privileged endpoints all pass - 🪄 Auto-installs the missing build dependency (
unrun) sopnpm run builddoesn't fail mysteriously
🚀 Quick Start
From zero to running (6 steps)
The core order in one sentence: install the dsh source first, then run apply.sh — apply.sh is a patching tool, not a dsh installer.
① Prepare the environment
- Node.js ≥ 24 (verify with
node -v) - pnpm (verify with
pnpm -v) - git
- 🪟 Windows users: run the bash scripts in Git Bash or WSL; the dsh source itself is cross-platform
② Get the dsh source
git clone https://github.com/deepseek-ai/deepseek-harness.git
cd deepseek-harness
pnpm install
⚠️ Must be a
git clonecheckout — the packagednpm installbuild won't work, because the patch modifies source files. ⚠️ Do not use a shallow clone (--depth 1) — the version pinning below needs the full history.The pinned target version is
0.1.2-rc.1(commita66e470204). If upstream has moved on (apply.shwill warn about a version mismatch), pin the version first:git checkout a66e470204 pnpm install
③ Clone deepseek-harness-lan and apply the patch
git clone https://github.com/oitsukiii/deepseek-harness-lan.git
cd deepseek-harness-lan
./apply.sh /path/to/deepseek-harness
apply.sh automatically: version check → verify the patch applies cleanly → apply → install the missing build dependency unrun. You're done when you see ✓ 补丁应用完成 (patch applied).
💡 Directory-independent:
deepseek-harness-lananddeepseek-harnesscan live in any directories (any combination) — the script locates its own patch and the dsh checkout by argument, with no hardcoded paths. Three ways to run: pass the dsh path as an argument,cdinto the dsh directory and run, or put deepseek-harness-lan next to dsh.
④ Rebuild the frontend
cd /path/to/deepseek-harness
pnpm run build
The randomUUID polyfill lives in the web bundle, you must rebuild for it to take effect.
⑤ Start and bind a LAN IP
pnpm dsh web --host 192.168.1.100 --trusted-host 192.168.1.100
Replace 192.168.1.100 with your LAN IP (check ip addr or your router's admin page).
⑥ Access and verify
On another device (Windows / phone / tablet), open in a browser:
http://192.168.1.100:3080
🔑 0.1.2-rc.1+ generates a per-launch session token. dsh prints the full URL including the token on startup, e.g.
dsh web: http://127.0.0.1:3080/?token=QvlADRxW9exunDHD7Wq6SiV53P7qsr-ERC_RT3PFW4AReplace
127.0.0.1with the LAN IP and open that URL on other devices. A new token is minted on every restart — grab it from the startup log again. (Without the token the server answers401.)
⚠️ If you opened it before, do a Ctrl + F5 hard refresh (the browser may cache the old JS).
Verification checklist:
- [ ] The page loads normally, title "DeepSeek Harness"
- [ ] Configuring a model API does not report 403 /
crypto.randomUUID is not a function - [ ] Other devices on the LAN can open it too
Revert (optional)
./revert.sh /path/to/deepseek-harness
Removes the patches with one command; afterwards you can git pull to update dsh normally.
🔀 Alternative: no source changes (SSH tunnel)
If you don't want to modify the dsh source (keeping it pristine for zero-maintenance official upgrades), an SSH tunnel is the only clean LAN access method — zero patches, all three gates pass naturally.
Why it works without patching
dsh's trust check (browser-trust) looks at the request's Host header, not the source IP (DNS rebinding defense); and crypto.randomUUID is only available in a secure context (HTTPS or localhost). An SSH tunnel makes the browser always access dsh via 127.0.0.1, so:
| Three gates | Under SSH tunnel | Why |
|---|---|---|
| Gate 1 (CLI/schema) | ✅ not triggered | dsh listens on 127.0.0.1 = official default, no changes needed |
| Gate 2 (browser-trust) | ✅ includes privileged endpoints | Host header = 127.0.0.1 → loopback exemption (the original privileged endpoints only allow loopback anyway) |
| Gate 3 (randomUUID) | ✅ available | 127.0.0.1 is a secure context; the browser provides it natively |
Steps
# 1. Start dsh as official (no patch, listening on 127.0.0.1:3080)
cd /path/to/deepseek-harness
pnpm dsh web
On the computer you want to access from, open the tunnel (Windows 10/11 ships OpenSSH):
ssh -N -L 3080:127.0.0.1:3080 <user>@<NAS-IP>
Open in the browser:
http://127.0.0.1:3080
API configuration, privileged endpoints, and all features work normally.
Comparison
| Option | Source changes | Computer | Phone/tablet | Official upgrades |
|---|---|---|---|---|
| SSH tunnel (this option) | ❌ zero changes | ✅ | ⚠️ configure a tunnel per device | direct git pull, seamless |
| deepseek-harness-lan patch (main option) | ✅ 3 minimal diffs | ✅ | ✅ open the URL directly | need to re-apply the patch |
Why nginx reverse proxy doesn't work
After a reverse proxy (HTTP or HTTPS), the Host header becomes the LAN IP, while the original privileged endpoints (settings.describe, llm.providers, etc. — required for configuring model APIs) use isTrustedApiRequest(request, []) which only allows loopback, so the configuration API step necessarily returns 403. The SSH tunnel is the only "no source changes + full functionality" path; if you want every device to open a URL directly, use the main patch option.
📌 0.1.2-rc.1+ note: privileged endpoints now honor
--trusted-host(upstream adopted gate 2). If your reverse proxy forwards a Host/Origin that you pass via--trusted-host, the config API works too — but dsh still rejects binding0.0.0.0, so the reverse proxy must connect to a specific bind address (e.g.--host 127.0.0.1proxied by nginx on the same machine).
🧠 How it works
dsh officially blocks LAN access via three gates. The project's patches break through them one by one (gate 2 is now upstream):
Gate 1: CLI rejects 0.0.0.0, schema only accepts two literals
Symptom: --host 0.0.0.0 fails immediately; binding a specific IP (e.g. 192.168.1.100) also fails to start:
ValidationError: invalid config:
- $.host expected "127.0.0.1" | "0.0.0.0" but got "192.168.1.100" (at host)
Cause (two layers of restrictions):
| Layer | Location | Restriction |
|---|---|---|
| CLI layer | packages/bundle/web-app/src/startup.ts | options.host === '0.0.0.0' triggers program.error() and refuses to start (left as-is: the wildcard stays rejected) |
| Schema layer | packages/host/webserver/src/index.ts | zod validates z.union([z.const('127.0.0.1'), z.const('0.0.0.0')]), and the type definition only allows those two values |
Patch (only the schema layer — the CLI layer already keeps the 0.0.0.0 rejection):
// packages/host/webserver/src/index.ts
- host: '127.0.0.1' | '0.0.0.0' // type
+ host: string
- host: z.union([z.const('127.0.0.1'), z.const('0.0.0.0')]).required() // zod
+ host: z.string().required()
Design trade-off: the 0.0.0.0 wildcard binds to every interface (including public/WG), which is dangerous; binding a specific IP exposes only one interface and stays controllable. So only specific IPs are unlocked — the wildcard stays rejected.
Gate 2: the /api browser-trust fence (403)
Symptom: the page opens, but every /api/* request returns HTTP 403, e.g.:
加载提供方目录失败: transport failure for /api/llm.providers: HTTP 403
Cause: there is a browser-trust fence in front of dsh's /api gateway (anti DNS-rebinding / cross-site attack) in packages/client/connection/src/index.ts:
// trust list: only when binding 0.0.0.0 does it collect all LAN IPs
// binding a specific IP → empty list → all non-localhost requests 403
const lanAddresses = bindHost === ALL_INTERFACES_HOST ? collectLanIps() : []
return { lanAddresses, trustedHosts: [...lanAddresses, ...extra] }
Additionally, privileged methods (settings.*, credentials.*, agentPreset.*, etc.) are checked against an empty trust list — meaning even with --trusted-host, those endpoints still only allow localhost:
if (PRIVILEGED_METHODS.has(method) && !isTrustedApiRequest(request, [])) {
return new Response('forbidden', { status: 403 })
}
Patch: none needed since 0.1.2-rc.1 — this gate was adopted upstream. packages/client/connection/src/rpc-host.ts now checks this.trustedHosts (configured from the CLI) instead of an empty list, so --trusted-host already applies to privileged endpoints. This repository therefore no longer patches packages/client/connection/src/index.ts; the patch file only covers the remaining 3 files.
Design trade-off: --trusted-host (the admin-configured explicit trust entry) also applies to privileged endpoints. Without --trusted-host, behavior is unchanged — still loopback-only, so the official security semantics are preserved.
Gate 3: crypto.randomUUID is not a function
Symptom: error when configuring APIs:
加载提供方目录失败: crypto.randomUUID is not a function
Cause: the browser Web Crypto API's crypto.randomUUID() is only available in a secure context (HTTPS or localhost). When accessing http://192.168.1.100:3080 (plaintext HTTP on the LAN), the function doesn't exist.
Patch (apps/web/src/main.ts, at the very top of the web entry):
// crypto.randomUUID is only available in secure contexts; add a UUIDv4
// implementation for plain-HTTP LAN environments
if (typeof globalThis.crypto === 'object' && typeof globalThis.crypto.randomUUID !== 'function') {
try {
globalThis.crypto.randomUUID = () =>
'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
const r = (Math.random() * 16) | 0
const v = c === 'x' ? r : (r & 0x3) | 0x8
return v.toString(16)
})
} catch { /* keep as-is in read-only crypto environments */ }
}
💡 The UUID is only used for local identifiers (attachment drafts, session ids, etc.) and is not involved in security checks, so the polyfill has no security risk.
🔒 Security notes (please read)
- ⚠️ This UI can execute arbitrary commands. After patching, any device on the LAN can access and control your agent.
- ✅ Only use it on a trusted home/office network.
- ❌ Do not port-forward 3080 to the public internet (port forwarding / DMZ).
- ❌ Do not use
--host 0.0.0.0(still rejected after patching — a deliberately kept line of defense). - 💡 Advanced: put an authenticating reverse proxy (e.g. nginx Basic Auth) in front of dsh.
- 🔐
--trusted-hostis the official browser-trust mechanism's explicit trust entry — only fill in your own LAN IP.
🧩 Compatibility
This patch is written and verified against dsh 0.1.2-rc.1 (commit a66e470204).
✅ Upstream note (0.1.2-rc.1): two of the original four LAN changes were adopted upstream — the CLI now has
--trusted-hostandpackages/client/connection/src/rpc-host.tschecksthis.trustedHostsinstead of an empty list — so the current patch set is down to 3 files:
packages/host/webserver/src/index.ts— widen the bind-host schema from two literals (127.0.0.1/0.0.0.0) tostring(LAN IPs);apps/web/src/main.ts—crypto.randomUUIDpolyfill for plain-HTTP LAN serving;packages/client/tsdown.client.ts—REPOSITORY_ROOTviaprocess.cwd()instead ofimport.meta.url(fixes the workspace manifest lookup when tsdown compiles this preset withunrunintonode_modules/.unrun, which otherwise breaks the build withno packages/*/*/package.json declares the name …).
⚠️ Known upstream build quirk (0.1.2-rc.1): on a clean checkout, the full
pnpm run buildmay fail withCannot find entry: ["lib/types/{index,invariant,startup}.js"]attributed to@deepseek-ai/dsh-root. Cause: leftover directories from older dsh versions (e.g.packages/client/runtime,packages/host/apiproxy) that were removed upstream but survivegit checkoutbecause they contain only ignored files (node_modules). Fix: remove the stale directories (git clean -ndx <dir>to list, then delete), then rebuild.
- Verification chain: patch applies cleanly (
git apply --check) → fullpnpm run buildsucceeds → Web UI page load →/apicalls → privileged endpoints (settings.describe, etc.) all pass. - dsh upstream iterates fast; other versions will most likely break:
- When the source context changes,
apply.sh'sgit apply --checkfails and aborts safely without dirtying your repo (this is a protection mechanism, not a bug).
- When the source context changes,
- Adapting to new versions:
- Run
apply.shon the new version and note the conflicting files when it fails; - Manually adapt by following the "three gates" approach in 🧠 How it works;
- PRs with adapted patches are welcome.
- Run
- Revert:
revert.shremoves the patches in one command; afterwards you cangit pullto update dsh normally.
🗺️ Version compatibility matrix
One patch targets one dsh version line; earlier/different versions may fail git apply --check (that's the safety mechanism — it aborts without dirtying your repo).
| dsh version | dsh Released | Which patch should I apply? |
|---|---|---|
| 0.0.1-rc.1 (npm) | 2026-08-10 | — |
| 0.0.1-rc.2 (npm) | 2026-08-11 | — |
| 0.0.1-rc.5 (npm) | 2026-08-12 | — |
| 0.1.0-rc.2 (npm) | 2026-08-13 | — |
| 0.1.0-rc.3 (npm) | 2026-08-13 | — |
| 0.1.0-rc.6 (npm) | 2026-08-13 | — |
| 0.1.0-rc.7 (release) | 2026-08-17 | v0.1.1 |
| 0.1.0-rc.8 (release) | 2026-08-19 | v0.2.0 |
| 0.1.1-rc.1 (release) | 2026-08-21 | v0.2.0 |
| 0.1.1-rc.2 (release) | 2026-08-21 | v0.2.0 |
| 0.1.2-rc.1 (release) | 2026-08-29 | v0.3.0 ⭐ current (3-file patch; 2 of 4 LAN changes now upstream) |
💡 Rule of thumb: versions rc.8 and newer are covered by the current patch; rc.7 and older need the older patch or an upgrade. Early releases (
0.0.1-rc.x,0.1.0-rc.2/3/6) were transitional within 3 days of the public launch — nobody should still be running them.
❓ FAQ
Q: Can I access it from my phone?
A: Yes. As long as the phone is on the same LAN, open http://<NAS-IP>:3080 in the browser.
Q: What happens when dsh updates officially after patching?
A: apply.sh runs git apply --check first; if the upstream code changed, it fails and aborts without dirtying your repo. Revert with revert.sh, then git pull normally.
Q: Why not run it with Docker? A: dsh is officially distributed as a Node.js / npm package with no official Docker image; this project only modifies source, so following the official install path is the most stable.
Q: Will the official project accept this patch? A: The official project deliberately blocks wildcard binding and is unlikely to relax it soon; but the "bind a specific IP + explicit trust" idea is worth an issue/discussion upstream.
🧩 Patch files
| File | Addresses |
|---|---|
| patches/deepseek-harness-lan.patch | unified diff of all 3 changes (use directly with git apply) |
🤝 Contributing
- Bugs / compatibility issues: open an issue with the dsh version + error message
- New platform / new version adaptation: open a PR, update the patch and verify on the target version
- Patch invalidated by upstream dsh updates: PRs with updated patches are welcome
🎉 Acknowledgements
- DeepSeek Harness — an excellent agent framework ("Everything is a Plugin")
- This project is developed with DeepSeek V4 Flash (troubleshooting → locating → patching → scripting → documentation, the whole flow)
📜 License
MIT License — an unofficial community project unrelated to DeepSeek Harness, for learning and personal reference only.
Comments
Loading…
Similar plugins
by GooDAnDReaDY
LAN network routing and browser media API compatibility helper for DeepSeek Harness Web UI
★ 5
↓ 1.9k/wk
MIT
JavaScript
Sep 17, 2026
dsh plugin --profile web add @goodandready/dsh-lanmodeby ayingQAQ
Windows 上的 DeepSeek Harness Web 一键启动器|One-click launcher with background restart and browser refresh.
★ 4
↓ 110/wk
MIT
JavaScript
Aug 18, 2026
dsh plugin --profile web add dsh-web-launcherby rice-awa
把 DeepSeek Harness 的 Web GUI 安全地开放到局域网或公网,自带密码鉴权,支持可选的TLS。Securely expose DeepSeek Harness's Web GUI to your local network or the public internet, with built-in password authentication and TLS support
★ 5
↓ 747/wk
MIT
TypeScript
Sep 13, 2026
dsh plugin --profile web add @riceawa/dsh-lan-gatewayby donoteatme
Browser-first, LAN-only access to the current DeepSeek Harness Web UI with one-time QR pairing, revocable devices, event-driven diagnostics, and a slot-preserving mobile layout; no companion app, clou
★ 0
↓ 182/wk
MIT
TypeScript
Sep 12, 2026
dsh plugin --profile web add dsh-local-linkby NaivG
Let Deepseek Harness access the internet seamlessly.
★ 0
MIT
TypeScript
Sep 12, 2026
dsh plugin --profile web add dsh-networkby Leon0555
一个DSH局域网内访问插件:让 DeepSeek Harness 可在局域网内被其他设备访问的 DSH 插件。同一局域网下,手机/平板/电脑打开浏览器即可直接访问你某台设备上的 DSH——无需 SSH、无需内网穿透,npm 一键安装。
★ 14
↓ 308/wk
MIT
JavaScript
Aug 24, 2026
dsh plugin --profile web add dsh-lan-access