DeepSeek Harness plugin

omdsh-remdev

Remote development for the DeepSeek Harness web GUI: connect a workspace to an SSH server, provision a .dsh-server there, run its files, terminals, and agents on that machine, and load its project

Jump to install

Source facts

Repository
omdsh-plugins/omdsh-remdev
Latest update
Aug 20, 2026
Category
UI Enhancements
GitHub stars
0
Format
plugin
Catalog evidence
Upstream dsh.bundle evidence
Evidence path
package.json#dsh.bundle
Checked against
0.1.0-rc.8
Upstream check date
2026-08-20

This evidence comes from the upstream catalog. This site has not installed, run, or security-reviewed the plugin.

Install

Start with a prompt that asks an agent to review the GitHub repository and source. Switch to the command if you want to install it yourself.

Copy this prompt into DSH, Codex, or another agent and ask it to review the GitHub repository and source first.

Do not install or run any commands yet. Read this plugin's GitHub repository, README, and relevant source code. Then answer the questions below clearly and directly so I can decide whether it fits my needs:

1. What is this plugin, and what problem does it solve?
2. Who is it for, and what are its typical use cases?
3. How is it used after installation? Include one minimal example.
4. What known limitations or privacy, security, compatibility, or maintenance risks does it have?
5. Give a clear recommendation: recommend, conditionally recommend, or do not recommend, with reasons.

Distinguish statements documented by the repository, inferences from source code, and unknowns. If evidence is insufficient, say so explicitly. Do not guess or simply repeat the README.

GitHub: https://github.com/omdsh-plugins/omdsh-remdev
Plugin: omdsh-remdev
Author: omdsh-plugins

Check the source files

Read the README and other files from this plugin directory before installing.

File explorer4 files
README.mdSource · read only
README language

omdsh-remdev

English | 中文

Remote development for the DeepSeek Harness web GUI: attach a workspace to an SSH server, and its files, its terminals, its conversations and its agents all run there — Work mode included, because Work mode is the harness on that machine, shown here.

What it adds

SurfaceWhere it comes from
A Remote connect button at the trailing end of the sidebar's Workspaces barAn entry in shell.overlay that renders nothing at its own seat and portals into [data-slot="sidebar.workspaces"]
The Remote development window — servers, provisioning, the folder picker, the live logsrc/client/RemoteDialog.tsx, over the /omdsh-remdev prefix route, fenced to the same trusted hosts as /api
The Plugin hub card — the server list, and the drawable tunables grouped by install / Code / remote window / conversations / credentialsAn entry in omdsh.plugin.card whose id is the package name; it replaces the generic form, which cannot draw the server list and would present twelve knobs as equals
On the server — a button in the session header of any conversation whose project is mounted, which is the one place Work mode could otherwise be quietly localAn entry in conversation.session.header.utilities, ui-conversation's own utility row (src/client/RemoteEnter.tsx)
The remote window — this app handed over to one server, Work and Code both running on itdsh --profile web started through the remote launcher, a tunnel over the SSH connection already open, and a loopback listener here (src/remote-web.ts, src/web-proxy.ts, src/client/RemoteWindow.tsx)
A globe badge on every remote workspace rowPortalled into [role="treeitem"][aria-expanded] and positioned from a measurement
The remdev service — remoteFor(cwd)ctx.reflect.provide, the one question omdsh-sidepanel and omdsh-codemode ask before reading a file or starting a process
A mounted project's skills (.dsh/skills, .agents/skills)src/remote-skills.ts, a provider registered on the harness's own skills service that reads SKILL.md files back over SFTP
A remote workspace that is an ordinary workspaceA real mirror directory under $DSH_HOME/remotes/<server>/<project>, which is what workspaces.create was always asking for
Conversations held on a server, listed and resumable hereThe session mirror, which copies the remote logs down and rewrites exactly one header field
The omdsh-remdev settings namespacectx.inject(['settings']), with the servers and their credentials in a hidden role('secret') store
The remdev.connect shortcut commandRegistered with ctx.shortcut when a keybinding layer is composed; the default chord is CmdOrCtrl+Shift+C (⌥⌘C in a tab), decided by omdsh-shortcuts' document — see src/client/shortcut.ts

A Remote connect button at the trailing end of the sidebar's Workspaces bar opens one window: pick a host, connect (the harness is installed if the machine does not have one), open a folder. What comes back is an ordinary workspace with a globe on its folder — the same rows, the same sessions, the same Code mode as a local project, on somebody else's computer. The same press is on the keyboard too: with omdsh-shortcuts composed, ⌘⇧C (⌥⌘C in a browser tab) opens the window, and the button's tooltip names the chord.

Nothing about local development changes. A composition without this plugin resolves no service; a workspace that is an ordinary directory resolves no mount. Both fall through the branch that was always there.

How a remote workspace is an ordinary workspace

workspaces.create canonicalizes its argument with realpath and requires a directory to be there. That is not an obstacle to route around — it is the harness saying a workspace is a place on this machine — so this plugin gives it one: a real, empty mirror directory under $DSH_HOME/remotes/<server>/<project>, holding nothing but a README.txt that explains itself to whoever finds it.

Everything downstream then works untouched. The session store groups by that directory, the registry accounts by it, the projections key on it, the usage roll-up sums by it. The only code that ever learns the directory is a stand-in is the code that was about to read a file or start a process in it, and it learns by asking one question:

const remote = ctx.get('remdev')?.remoteFor(cwd)
const listing = remote === undefined
  ? await listLevel(cwd, path)      // exactly as before
  : await remote.list(path)         // the same listing, over SSH

That is the whole of the seam. omdsh-sidepanel and omdsh-codemode each gained one structural view of it and one branch; neither imports a value from this package, and neither knows what SSH is.

A mounted project's skills

The harness discovers skills from the session's cwd: its own filesystem provider walks <project>/.dsh/skills and <project>/.agents/skills through the LOCAL filesystem service. A remote workspace's cwd is its mirror directory — an empty local stand-in — so the server's project skills are invisible to an agent running here. This plugin registers a provider (remdev) on the harness's skills service to close exactly that gap: when the cwd resolves to a mount, it reads the two skill directories back over SFTP into the same catalog — directory bundles and flat Markdown skills alike, parsed with the harness's own frontmatter grammar (name, description, whenToUse, invocation policy, metadata).

Four things are worth writing down:

  • It reads two fixed paths inside the mount. The fence is the same one the file browser uses; a skill can never reach anything outside the mounted directory, and an unreadable skill file is skipped with a log line rather than failing the rest.
  • A skill's resourceBase is opaque, not directory. The templates and scripts a skill references live on the server, where the local file tools cannot read them, so the model is told where they are and how to get there instead of a path its read tool would fail on. To use those resources, open the remote file panel or a remote terminal; a Code-mode agent already runs on the server and reads the same .dsh/skills locally there.
  • Remote changes are not watched. The catalog re-reads when the registry's cache invalidates — a changed mount table (attached or detached folders, edited servers) does that — so a skill added on the server appears on the next refresh, and the same goes for a skill file edited mid-session.
  • An unreachable server simply contributes no skills. The harness marks the observation incomplete and retries on a later boundary; user skills in the local ~/.dsh/skills are unaffected.

.dsh-server

The layout is .vscode-server's, for the reason VS Code chose it — a remote install has to survive a server having its own Node, the wrong one, none, or several:

<dshHome>/.dsh-server/
  bin/dsh              the launcher every terminal actually runs
  node/<version>/      a private Node, present only when the system one is unusable
  tools/pnpm/<v>/      a private pnpm, present only when the system one is unusable
  tools/bin/           shims for the above, on the launcher's PATH
  cli/<version>/       npm prefix holding @deepseek-ai/dsh and its graph
  data/                DSH_HOME for the remote harness: profiles, sessions, storages
  install.log          the last provisioning run, for when somebody asks why

bin/dsh is the whole contract with the rest of the plugin. Terminals run that path and nothing else, so which Node was chosen and which release was installed are decided once and repeated nowhere.

Two ways in. The server downloads Node and installs the package itself; failing that, this machine stages both and pushes them over SFTP. The fallback exists because the servers people most want this for — an internal cluster node, a machine behind a jump host — are exactly the ones with no route to nodejs.org. It moves ~100MB, so it is never the first choice and always the second.

Failing has to be something the first route can actually do. A download that fails is uninteresting: the fallback runs and the whole thing takes seconds. A download that hangs is the real case, and it is the normal condition on a machine behind a firewall that drops packets rather than refusing them — the connection to the server is healthy, so keepalives see nothing wrong, and curl's own defaults are 300 seconds to connect and then forever to transfer while wget waits forever at both ends and retries twenty times. So every download carries --connect-timeout and --max-time (--timeout and --tries=1 for wget), and every command that reaches the network carries a backstop clock in the transport behind those flags. Without them the fallback is unreachable in practice on exactly the servers it was written for.

And a person can say it outright. Ticking Upload from this machine beside Connect skips every server-side download and stages the lot here. It changes nothing about what gets installed — only which route is tried first — and it exists because waiting through four steps' worth of timeouts to prove something you already knew is its own kind of failure. tuiLocalPath goes one further: it names an omdsh-tui checkout on this machine to build and push, which is the only arrangement that works when neither end has a route.

pnpm is vendored too, for a reason worth writing down. dsh plugin is a thin forwarder: it spawns pnpm by name off PATH and cannot be pointed anywhere else. Node's own tarball ships node, npm, npx and corepack — and no pnpm. So the very servers this plugin vendors a Node onto are exactly the servers that then cannot be given a terminal application: dsh: pnpm not found on PATH, exit 127, and a Code profile left holding nothing but dsh-base. A pnpm goes under tools/pnpm/<version> by the same two routes as Node, with a shim at tools/bin/pnpm that the launcher puts on PATH.

The install is an unpack, not an npm install -g, and both halves matter. pnpm publishes with no dependencies and its whole runtime in dist/, so untarring it is installing it — which keeps npm's ~500MB dependency-resolution heap off a small server, on a 1GB box that has already been watched to die of exactly that. And it lands under .dsh-server rather than in a global prefix, because npm i -g with a system Node writes into /usr/lib/node_modules: root's to give, nobody's to take back. Removing .dsh-server still removes everything. A server with a pnpm 9 or newer of its own keeps it and downloads nothing.

The shim execs the chosen Node by absolute path rather than letting #!/usr/bin/env node resolve — otherwise a vendored pnpm ends up running on the very system Node that was too old to run the harness.

Everything is asked through a login shell — one that starts. ssh host command runs non-interactively and non-login, and on a real development server that is a different PATH from the one a person sees when they log in — nvm, conda, homebrew, and every module load system put their toolchain there. Measured through a plain sshd: sh -c finds no node, sh -lc finds it. Without the -l, a server with a perfectly good Node 22 is probed as having none and gets a second copy downloaded onto it.

Which login shell is the part that bites. /etc/profile sources every /etc/profile.d/*.sh unconditionally, those files get written by people who tested them in bash, and /bin/sh on a Debian-family server is dash — where one function foo() {} is a syntax error, which POSIX says a non-interactive shell must exit on. Not a warning, not a skipped file: sh -lc exits 2 while sourcing the profile and the script never runs. So the shell is chosen on the server, by asking each candidate to start a login session and say nothing: $SHELL when it is a POSIX one, then bash, then sh, then plain sh -c with no login at all. The payload appears once in that command and runs once, so an npm install cannot be run twice by a fallback. A degraded PATH is worth strictly more than a connection that cannot be probed.

The probe prints a marker line before its own output and the parser reads the fields after the last one, because a login shell runs the machine's profiles and profiles talk — a conda banner ahead of the answer would otherwise be read as the server's home directory.

Code mode needs a terminal application, and nobody should have to ask for one. The harness ships three profile bundles and none of them is an interactive terminal, so provisioning also creates a profile (dsh-code by default) composing @deepseek-ai/dsh-base with whatever codeBundle names — @omdsh-plugins/omdsh-tui-app unless you say otherwise — and installs it. There is no separate button for the terminal and no separate profile for it to live in: a server that has been set up has a terminal on it, or a reason it has none.

Which install runs is decided by where the package comes from. A terminal your registry carries is added with dsh plugin add, run THROUGH the launcher so it lands in the right DSH_HOME, on the right Node, and on the PATH the vendored pnpm shim is on. The default one is carried nowhere — omdsh-tui publishes nothing: no npm package, no releases, no tags — so it is installed the way its own README says to, as a checkout install: fetch the default branch's current head, install the dependencies, build, link both packages (@omdsh-plugins/omdsh-tui and @omdsh-plugins/omdsh-tui-app) into the Code profile, and then prove it — the profile must compose with the bundle's layer in --dump-config, and every module a loader row names must resolve from the profile directory, which is the check that catches a tree that dumps perfectly and dies at boot.

The same two routes as everything else: a server with internet fetches and builds on its own; one without gets the source downloaded, installed and built on this machine — with pnpm's hoisted node linker, so the tree that goes up holds real files rather than symlinks into this machine's store — and the built tree pushed over SFTP. The checkout lives under .dsh-server/tui/, so removing .dsh-server removes it too.

"Upload from this machine" moves the build, not the download. The source still comes from GitHub — this machine fetches it instead of the server — so a tick in that box does not make the install offline. And the URL is the most rate-limited thing an unauthenticated client can ask GitHub for: a tarball generated on demand for an unpinned ref. Pressing Reinstall a few times in a few minutes is enough to be answered HTTP 429.

Which used to end the install outright, with a perfectly good copy of the source sitting in the staging directory beside it. Now a refusal that means "ask again" — 429, any 5xx, a dropped connection — is waited out and retried up to three times, honouring Retry-After where the server sends one and capping it so a bad day cannot park an install for an hour. If it still cannot be had, the previously staged tarball is used and the log names the day it was downloaded: yesterday's terminal is worth immeasurably more than no Code mode, and nobody should have to wonder which source they are looking at. It is never preferred — Reinstall means "get the latest", and this runs only when the latest cannot be had.

tuiLocalPath is the way to stop depending on that download at all: point it at a checkout on this machine and nothing is fetched from anywhere.

A server that is missing its terminal is noticed by the thing that needs one. Opening a remote Code terminal asks the profile what it composes — one cat of its manifest, once per server per process — and a profile holding dsh-base and nothing else gets the terminal installed rather than a terminal started that would die at boot. The TERMINAL, not the server: provisioning resolves dshVersion first, so making a full run the repair would have opening a Code column silently upgrade the harness on that machine. That check is what the bundle ROWS are for: a profile whose creation succeeded and whose install then failed looks exactly like a finished one from the outside, and treating it as finished is what leaves the one machine that needs an install the one machine that never gets one.

What that costs is bounded on purpose. An automatic run installs only what is missing, so a server that already has its terminal never rebuilds one. A failed attempt is remembered as a failed attempt: the next terminal is refused with the reason rather than starting minutes of failing install all over again. And the failure never takes the rest down with it — files, shells and mirrored conversations do not need a terminal, so provisioning finishes and reports.

Reinstall on a ready server is the update. It is the one run a person asked for, so it re-installs the terminal whether or not one is there: the checkout is replaced with the branch's current head, the profile re-linked, the verification re-made — and for a registry terminal, resolved again from your registry. That is why the button is worth pressing on a server that looks fine, and why it takes minutes when it does.

Work mode runs on the server

Everything else here makes a LOCAL harness reach a remote machine: files over SFTP, terminals over a pty, a project's skills over both. Work mode cannot be done that way, and for a while this plugin pretended otherwise.

A Work conversation IS the harness's agent loop — its read, its edit, its bash, its session log. On a mounted workspace that loop was running here, in the mirror directory, which is an empty local stand-in: glob found one README.txt, bash ran on the laptop, and an edit landed on a file the server had never heard of. Code mode had been remote since the beginning (it starts the remote launcher), so the two halves of the same workspace disagreed about which computer they were on.

So the harness that serves Work on a mounted project is the one on that machine. There are two ways in and they mean the same thing: On the server, in the session header of a conversation whose project is mounted — where the question actually comes up — and Enter server on that machine's row in the Remote development window. Either one hands the screen to it: dsh --profile web runs there, bound to that machine's own loopback; a tunnel rides the SSH connection that is already open; a loopback listener here is what a browser points at. What fills the frame is the server's own dsh — its conversations, its Work column, its files, its agent, its terminal — and the strip along the top is the only part of the screen that is still local: which machine you are on, and the way back out.

That is VS Code's arrangement, for VS Code's reason: the work happens where the code is, and the interface happens where the person is.

The server needs nothing installed for it. web is one of the harness's own shipped profile templates (dsh-base plus dsh-web-app, both in-box), and booting a profile that does not exist yet initializes it from that template. So a server this plugin has provisioned can already serve the GUI: no bundle to add, no package to fetch, no registry to reach. It is the one remote surface with no install step — which is also why entering a server does not ask for the terminal application Code mode needs, and works on a machine whose omdsh-tui build failed.

The port is read, not chosen. --port 0 lets the OS pick, and the harness prints `dsh web: http://127.0.0.1