Remote development over SSH: pick a directory on a registered machine as the session workspace and the file, shell, bash, and search tools run on the remote host — the agent works there with the same local tools, and no extra model-facing tools are added.
Install
# from GitHub (first run asks for allowBuilds approval — follow the hint, retry)
dsh plugin --profile web add github:CJYLZS/dsh-remote-development
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
English | 中文
Summary
This plugin adds lightweight remote development to DeepSeek Harness: you register an SSH machine, pick a remote directory as the session's workspace, and the agent then works on that remote workspace with the SAME tools it uses locally — file tools, shell, and search. The plugin does not add any model-facing tool and no third-party UI plugin: it replaces the filesystem, subprocess, and bash providers with routing versions that translate local tool calls into remote execution over SSH, and it contributes one settings section plus one workspace directory-flow dialog in the Web GUI.
Highlights
- Identical toolset. Remote workspaces add no tool calls: the agent works with the exact same tools as a local workspace, and the plugin translates those calls to remote execution underneath.
- Zero remote dependencies. The remote machine needs no extra server-side component — an SSH connection is all it takes.
- Web GUI integration. Connection management covers password / private key / SSH agent, jump hosts, host-key TOFU, and connection testing; remote workspaces can be told apart by a custom per-machine color.
Table of Contents
- Highlights
- Install
- Usage
- Understand the design
- Configuration
- Known Limitations and Deferred Work
- Dev Note
Install
From GitHub (recommended) — the built lib/ is committed, so it is one command with no build step:
dsh plugin add --profile web github:CJYLZS/dsh-remote-development
For development, link a local checkout instead:
cd dsh-remote-development
pnpm install # self-contained workspace; store lives in .pnpm-store/
pnpm run build # emits lib/index.js (host) and lib/client.js (browser)
dsh plugin add --profile web link:/absolute/path/to/dsh-remote-development
A link: install points the profile at the checkout directory, so later pnpm run build runs apply on the next harness restart without re-adding.
Restart the harness after installing.
Usage
Three steps and nothing else:
- Add a machine. In the Web GUI's dsh-remote-development settings section, enter host, port, and username, choose password / private key / SSH agent authentication (optional jump proxy), and click test.
- Pick a remote directory. In the workspace directory flow (the hero "choose directory" dialog or the sidebar workspaces picker), open the Remote tab, browse the machine's directories, and set one as the session workspace.
- Work as usual. That is the whole setup. File tools, shell, bash, and search run through the same tool calls as before — only now they execute on the remote machine; the model's working directory is the remote path, so it needs no special instructions and gains no new tools. Anything outside the chosen directory keeps local behavior, so existing sessions are untouched.
Under the hood, setting a remote workspace creates an anchor under $DSH_HOME/remote-workspaces/<host>-<user>-<port>/<basename> — a real local directory whose metadata records the remote coordinates. There is no "current machine" and no default target: the anchor alone decides where its session's tools execute. If that machine is later deleted, operations on the workspace fail with an explicit "machine is no longer configured" error instead of silently running elsewhere — re-add the machine to resume, or delete the workspace directory.
Understand the design
Three routing providers replace the base row of the same service, so every local tool keeps working and only the transport changes:
RoutingFileSystem(replaces the sandbox filesystem) — file reads, writes, edits, and listings whose path resolves to a remote root go through SFTP; everything else delegates to the local base viasuper().RoutingSubprocessRuntime(replaces the local subprocess runtime) — spawns with an anchor cwd execute on the remote host over an SSH exec channel; the packaged ripgrep used by search tools is rewritten to the remotergbinary. Routing keys on the workdir alone, so behavior is identical on every host platform.RoutingBashExecutor/RoutingPwshExecutor(replace the sandbox bash/pwsh executor) — commands with a workdir under an anchor run throughbash -con the remote host, and background processes get a real remote PID via a process-group kill protocol. The host platform picks which executor mounts — only the LOCAL fallback is platform-bound (local bash on POSIX, local pwsh on Windows); the remote host's POSIX shell always decides the remote dialect.- Per-agent tool visibility. The patch mounts both shell tool stacks, and a scoped restriction per session hides the dialect the session's workspace must not use: a remote session sees the
bashtool (neverpwsh), and a local session on a Windows host seespwsh(never the plugin-addedbash). POSIX local sessions keepbashfor both worlds, matching the base composition. - One shared SFTP session per machine. The SFTP protocol multiplexes every request over one subsystem channel, so all file operations share a single session instead of opening (and leaking) a channel per call — servers cap sessions per connection, and exhausted caps answer every open with a channel failure.
- Remote workspaces are marked in the file tree. The client recolors folder icons for remote workspaces: the host joins every anchor with its machine's marker color (a
Machinefield, editable in settings), and the client turns that list into attribute selectors over the tree'sdata-files-*hooks, injected as a stylesheet. One color per machine; leaving it empty falls back to the theme accent. The sidebar's workspace rows expose no data hooks, so their rules reach the row through itsaria-labels (:has()), matching the workspace title an anchor workspace adopts from its directory basename. - Remote workspaces display their remote path. The shell shows a workspace's directory — the tree's root header and the workspace list's hover card — as the session's working directory, which for a remote workspace is the plugin's local anchor directory. The client keeps an alias table (anchor directory → remote path, from the same
/statusjoin the marker color uses) and rewrites those display texts to the remote path through a mutation observer, touching only existing text nodes so React's reconciler keeps every element it owns. The underlyingdata-files-*coordinates stay local: the tree still navigates by them.
The model never sees the anchor handle. The harness's system prompt reports the session's working directory; for a remote session the plugin overrides that variable per agent with the remote path, so the model-visible cwd is the directory its commands actually run in. As a safety net, anchor-directory spellings (absolute, ~, $HOME, ${HOME}) in command text are rewritten to their remote paths before execution — same-machine anchors only, stdin left verbatim. Local sessions pass through unchanged.
Remote mutation policy mirrors the local sandbox: read-only mode rejects remote writes; workspace-write allows writes only under the remote workspace root and the remote /tmp. Remote writes are serialized per file, publish atomically (temp file + rename), and refuse stale versions and ambiguous edits with the same error codes the local filesystem produces.
Host keys use TOFU (accept-new by default): the first-seen key is recorded, a changed key is rejected with a MITM reason, and verify/off modes are available per machine.
Configuration
Machines are managed in the settings section; the plugin itself takes config defaults through cordis.yml (all optional):
| Field | Default | Meaning |
|---|---|---|
commandTimeoutMs |
20000 | Per-command timeout; the channel is closed after SIGTERM grace. |
connectTimeoutMs |
15000 | SSH connection establishment timeout. |
maxOutputChars |
200000 | Command output kept head-plus-tail beyond this size. |
maxFileBytes |
52428800 | Largest file read or written through SFTP. |
hostKeyMode |
accept-new |
accept-new, verify, or off. |
remoteRipgrep |
rg |
Remote binary the packaged ripgrep is rewritten to. |
anchorRoot |
$DSH_HOME/remote-workspaces |
Root directory for anchor directories. |
auditLog |
off | Append-only JSONL audit of remote executions. |
Known Limitations and Deferred Work
- Windows remote hosts are deferred. Remote machines must run a POSIX shell;
unamedetection rejects Windows targets with a clear error. Adaptation is reserved for a later phase. - Windows local hosts route remote shell commands. A Windows host mounts the pwsh-based routing executor: local workdirs keep the sandboxed pwsh executor, and anchor workdirs cross to the remote host's bash. The model sees the
bashtool for remote sessions and thepwshtool for local ones. - Persistent terminal sessions are not supported. The terminal tool reports an explicit "not supported by dsh-remote-development" error instead of letting the agent try; use the bash tool for remote commands. Persistent shell tools remain local-only: a persistent tool pointed at a remote workspace refuses with the same error.
@file references are not supported in remote sessions. Typing@in a remote session yields a single explicit "not supported yet" candidate rather than a silent failure; the reference-source interface is reserved for a later phase.- No mirror or sync layer. Anchor directories hold metadata only, not file copies; every read and write crosses SSH, bounded by
maxFileBytes. - Tree marking depends on in-box
data-files-*hooks. The recoloring targets the file tree's data attributes, which are not a declared public contract; a dsh rename silently drops the coloring (purely presentational — nothing else breaks). Workspace-row markers additionally match by workspace title: renaming a workspace, or a dsh change to the sidebar'saria-labelcopy, drops the row marker while the file-tree marker keeps working. - Path aliasing depends on display structure. The remote-path rewrite targets the tree root header's span shape and the hover card's portal shape, both undeclared; a dsh change to either silently leaves the local anchor path visible (purely presentational — navigation, addressing, and the copy action are untouched and still use the real local directory).
- Search needs a remote ripgrep. The
rgbinary must exist on the remote machine (configurable viaremoteRipgrep); otherwise search tools fail on remote paths. - SSH runs on pure JS, not native crypto. The bundled
ssh2never loads its optional native accelerators, so throughput on large SFTP transfers is lower than a natively-builtssh2would give. - Not published to npm. Install from GitHub or a local checkout — see Install.
- The built-in directory-picker flow is shadowed, not replaced. Both directory-flow registrations coexist at distinct priorities (this plugin uses -1, lowest renders); unloading this plugin hands the slot back to the built-in picker.
- The 本机 (local) tab follows the host's composed picker capability. The host resolves its directory-picker backend once at boot: a WSL without zenity/kdialog, an SSH launch, a non-loopback bind, or a display-less Linux all compose the
browsebackend (only thelist/createDirectoryprimitives — no OS chooser). The local tab branches on that resolution —nativeopens the OS chooser,browsedrives the host's in-app web browser instead; before this, the tab hard-codedpick, which fails withdirectory-picker/unavailableon such boots. - Deleting a machine strands its workspaces on purpose. Anchors survive machine deletion, and every tool surface refuses them with the same "no longer configured" error (fs, bash, subprocess, and the prompt's cwd variable falls back to the local handle) rather than executing on another machine or locally.
Dev Note
The plugin directory is a self-contained pnpm workspace (packages: [- .], storeDir: .pnpm-store) so pnpm cannot reach the harness repository's workspace. dsh framework packages are declared as peerDependencies (^0.1.2-rc.1, supplied by the host profile) and pinned exactly in devDependencies for local types and builds; no relative link: dependencies exist inside the dependency graph, so the directory builds standalone in any location.
Commands: pnpm run build (tsdown, both halves), pnpm run typecheck, pnpm run test (node:test via tsx; no SSH server needed — the pool accepts an injected client factory and the SFTP surface is faked).
ssh2 is a devDependency because it is build input, not a runtime dependency: pnpm run build bundles it into lib/index.js. Commit the rebuilt lib/ with any source change, or the GitHub install serves stale code.
Third-party code
lib/index.js contains bundled copies of ssh2 (MIT), asn1 (MIT), safer-buffer (MIT), tweetnacl (Unlicense), and bcrypt-pbkdf (BSD-3-Clause). Their license texts are reproduced in THIRD-PARTY-NOTICES.md.
Bundling moves security updates onto this repository: an ssh2 advisory no longer reaches users through their own pnpm update. Patching it means pnpm update ssh2 && pnpm run build, then committing the result here.
Links
More in this category
zhu1090093659/dsh-web-ui#packages/dsh-ssh★ 7488
SSH ops panel for DSH: web terminal, SFTP transfer with progress, local port forwarding, and one-command cluster execution across hosts; agents share the same host config.
zhu1090093659/dsh-web#packages/dsh-remote-web-ui★ 7488
Remote control of a dsh web workspace from phone or PC: QR-code pairing through a token-gated channel, SSE real-time sync, and separate mobile and full desktop GUI modes.
ZSeven-W/dsh-ios★ 288
A live iOS Simulator or USB-connected iPhone inside the conversation: 22 agent tools for booting, building, driving the UI by accessibility identity or OCR text, list-row actions and SwiftUI preview hot reload, plus a streaming sidebar panel you can tap and drag on.
saya-ch/dsh-mobile★ 260
Access DeepSeek Harness from the Android app or a mobile browser with secure LAN and remote connections, persistent device pairing, and a customizable mobile interface.
liguobao/deepseek-harness-remote★ 183
Multi-device remote access for DeepSeek Harness: continue an active session from your phone, tablet, browser, or another computer over an end-to-end encrypted channel (Noise IK + adaptive relay/WebRTC transport), with device authorization, ApiProxy-only remote capabilities, and read-only file preview via dsh-file-viewer — no shell, remote desktop, or write access.
wenbin-wb/dsh-bridge★ 158
Remote and mobile access for DeepSeek Harness: provides LAN QR code connection, Cloudflare/custom tunnels, WeChat, QQ, Feishu, Telegram bot integration, and security authentication.
Community comments
Comments are public GitHub Discussions. Loading them connects to GitHub and Giscus; a GitHub account is required to post.