dsh-remote-ssh: A DSH plugin to treat remote machines as local.

Introduction

By default, the tasks run by DSH (DeepSeek Harness) are executed in the local workspace: agents read and write local files, and the terminal runs locally. Once the target machine for a task is on another server, you have to manually shuttle things back and forth between the local workspace and the remote SSH session—opening multiple terminal windows, manually running scp, and relying on memory for port forwarding. There is another approach: simply moving the entire harness to the remote end to execute. This plugin takes a middle ground: the local workspace remains the execution ground, and the remote machine is its “shadow”; the panel, terminal, files, synchronization, and forwarding are all consolidated into the DSH sidebar.

Below is an introduction to dsh-remote-ssh, maintained by LinHao100076 (Repository LinHao100076/dsh-ssh-remote, 3 GitHub stars, categorized as a client, MIT license). It is not a generic SSH client, but rather a “Remote SSH” panel added to DSH, covering the file tree, remote terminal, edit synchronization, transfer resume, and three types of forwarding: -L / -D / -R.

What is it

A one-sentence positioning: browse the remote file system, open a remote terminal, synchronize local changes according to mapping directions, and establish port forwarding between the local machine and the remote machine in the DSH sidebar. Maintainer LinHao100076, public repository, MIT license.

The repository’s positioning is clearly stated in its own documentation (see “Core Rationale” in docs/design.md and README.md):

  1. This is not an SSH client, but a “remote shadow of the local workspace,” because the agent and the sidebar operate on the local workspace.
  2. DSH has an execution world abstraction (ctx.fs / ctx.subprocess), theoretically allowing the entire harness to be moved to the remote end; however, dsh-better-sidebar bypasses ctx.fs to read directly from the local, and mixing these two approaches would result in a split-brain.
  3. Therefore, version 0.x adopts a “local mirror + sidecar remote channel” architecture; replacing the execution world is reserved as an independent profile experiment for version 1.0.

Version status: The package.json shows 0.7.0; 0.7.0 in CHANGELOG.md is dated 2026-11-06. The main body of the README still says “Current version 0.6.1 — the first stable release,” which is inconsistent between the two locations, with the actual repository files taking precedence. The main increment in 0.7.0 is password / OTP authentication and multi-hop jump chains; 0.6.1 fixed the issue where the host bundle crashes immediately upon loading on Node 20, 0.6.0 added SOCKS5 dynamic forwarding and -R, 0.5.0 added resume transfer, and 0.4.0 added remote search.

Entry Points and UI

The plugin provides a “Remote SSH” panel that appears in two locations, depending on whether dsh-better-sidebar is installed:

Installed Appears in How to open
Has dsh-better-sidebar A tab named “Remote SSH” in the better-sidebar sidebar Click “Remote SSH” in the sidebar
No / dsh-better-sidebar disabled DSH’s built-in right sidebar, same panel Server icon button in the top right of the session header

The two paths are mutually exclusive: if dsh-better-sidebar is activated after startup, the plugin will remove the native entry point and replace it with the same tab, so two entry points will not appear simultaneously. The panel consists of a host header + a tab bar (Files / Terminal / Sync / Forwarding / Settings) + a status bar. The width follows the right sidebar with a 424px baseline. Dark mode reuses the same tokens and automatically follows the DSH theme. The UI baseline is ui/prototype.html in the repository (a single file that can be opened directly, supporting direct links like ?view=terminal).

Installation and Activation

The installation method provided in the README is the published package + dsh plugin command; it does not provide direct installation in the form of dsh plugin add github:owner/repo:

npm run pack:rel     # Generates dist/dsh-remote-ssh-<version>.tgz
dsh plugin --profile <profile> add -w "/path/to/plugin.tgz"

Two points to note: -w cannot be omitted; the profile contains pnpm-workspace.yaml, and omitting it will report ERR_PNPM_ADDING_TO_ROOT; do not point to the extracted directory after unpacking the tgz, as pnpm will write link:, and the linked directory will not install its own dependencies. The path via unpacking has also been available since 0.3.1—the published package has zero runtime dependencies, and ssh2, ws, chokidar are all bundled into lib/server.mjs (about 1 MiB) by esbuild.

After installation, package.json will become:

"dsh": { "profile": { "bundles": ["@deepseek-ai/dsh-base", "@deepseek-ai/dsh-web-app", "dsh-remote-ssh"] } },
"dependencies": { "dsh-remote-ssh": "file:C:/.../dsh-remote-ssh-<version>.tgz" }

The plugin stores hosts and mappings in ~/.dsh/storages/dsh-remote-ssh/state.json. Passwords and passphrases are never written to disk and only exist in the browser’s memory, retrieved fresh for each connection.

Startup command (app arguments follow directly, do not write web):

dsh --profile <name> --no-open --port 4987

The desktop profile is exclusively managed by the desktop application; the CLI will reject it (profile "desktop" is managed exclusively by the Electron application). For this profile, please install it in the plugin page within the application, or manually modify bundles in package.json and cordis.patch.yml before restarting the application. During development, to connect directly to the source directory, you can simply insert into cordis.patch.yml:

- insert:
    - id: dsh-remote-ssh
      name: "dsh-remote-ssh"

Core Features

The file view provides a remote file tree, text preview, and upload/download. The magnifying glass in the toolbar runs rg (or grep -r if absent) on the remote machine. It only brings back the matched lines, without downloading the files or building a local index, so the searchable scope is always larger than the mapped directories.

  1. Default literal + smart-case: Inputting foo ignores case, inputting Foo is case-sensitive, Aa forces case-sensitivity, only .* is interpreted as a regex.
  2. Results are grouped by file; clicking one opens the file and scrolls to that line; extremely long lines are trimmed around the match.
  3. The default scope is the current directory; rg respects .gitignore; when rg is absent, grep explicitly skips .git, node_modules, .venv, __pycache__, target, dist, build, .next, .cache, and .idea.
  4. There is a limit of 200 results, clearly marked as truncated; truncation occurs on the remote side (head -n).
  5. The output channel uses rg --json / grep -rInZ to avoid parsing errors when paths or content contain colons; when older rg does not recognize --json, it degrades to grep and remembers this.

Terminal and tmux

When tmux exists on the remote, it automatically executes tmux new -A -s dsh-<host>, reconnecting to the same shell after page refreshes, IDE restarts, or network jitter. When tmux does not exist on the remote, it falls back to a normal login shell and clearly warns above the terminal: “After refreshing the page or disconnecting, the processes of this session will end,” rather than pretending everything is normal. Both branches are covered in test/e2e-profile.mjs, including the actual argv shapes sent.

Authentication and Jump Chains

0.7.0 added password login and keyboard-interactive (2FA / OTP): servers with only PasswordAuthentication enabled, and servers that only ask for keyboard-interactive, can both be connected; if the server asks multiple questions in a row, multiple boxes will pop up; after guessing wrong once, it switches to asking a person directly, and when the background reconnection encounters a scenario needing credentials, it returns needs-credential instead of getting stuck on an invisible dialog box.

Jumping is not a separate page but part of the host parameters. It supports multi-hop, each hop having its own port, account, and authentication method; the summary line looks like 2 hops bastion-a:22 → bastion-b:2222; ProxyJump a,b in ~/.ssh/config expands into two hops, and the entire chain is written back as editable hops after the first connection. Each hop remembers its own TOFU fingerprint (switching jump host keys is not treated as switching target machine keys). Fingerprints are saved per hop, passwords, and target machine passwords are stored separately in memory, and neither is written to config.json.

Sync Semantics

Sync direction is determined by the mapping, with the following rules:

  • Default is one-way local → remote; files modified by the agent are automatically pushed up; only after enabling bidirectional will remote changes be downloaded.
  • The local machine is the truth: conflict determination = local modified AND remote mtime drifts more than 2s from the baseline; conflicts are only reported, and the UI offers “Keep Local / Keep Remote / Ignore”.
  • Deletions are not propagated: local deletions are only noted in the queue, never executed as rm on the remote.
  • Writing uses a temporary file + rename to land, avoiding reading half a file; the remote executable bit is pushed according to the local mode (supplemented by modeTemplate on Windows).

Resume Transfer

Transfers are written sequentially in 1 MiB chunks, not the whole package fastPut / fastGet:

  1. Both directions first write to part files in the same directory (<name>.dsh-part-<size>-<mtime>), only renaming to the real name after completion.
  2. The size of the part file is the breakpoint: on retry for the same file after interruption, disconnection, or process restart, it continues from the end of the part file, only sending the remaining part.
  3. The part file name includes the source file’s size and mtime; if the source file changes, the fingerprint changes, and old parts will not be used for splicing, preferring to retransmit.

Port Forwarding

The shape of a rule in the forwarding tab is “one end listening → one end target,” and the type determines which end is on the local machine:

Type Listening Target Equivalent to
Local Local machine Remote host:port ssh -L 6379:127.0.0.1:6379 me@server
Dynamic Local machine (SOCKS5) Determined per connection ssh -D 1080 me@server
Remote Remote machine Local host:port ssh -R 8080:127.0.0.1:3000 me@server

Behavior details:

  • The bind address defaults to 127.0.0.1; selecting 0.0.0.0 will be marked in red at the top of the panel—for -L / -D it exposes the remote service to the entire local area network, and for -R it exposes your local machine to the network where the server is located.
  • Default is auto-start with the host connection, but can be turned off to manual.
  • It uses the SSH connection already established by the host (-L / -D uses direct-tcpip, -R uses forwarded-tcpip), requiring no secondary authentication.
  • Disconnection recovery: the local listener is not destroyed when the connection drops; subsequent inbound connections trigger lazy reconnection; for -R, the listening port exists on the server and is gone when disconnected, automatically re-applying for it after reconnection, and the panel displays “Port on server temporarily does not exist” when not yet re-applied.
  • Port conflicts are determined by “listening end + port”; 0.0.0.0:8080 and 127.0.0.1:8080 are considered conflicts; -L and -R using the same port number are not considered conflicts.
  • Stopping a rule or disconnecting the host closes the listener and cuts the tunnel; -R also notifies the server to stop listening (cancel-tcpip-forward).
  • SOCKS5 only implements CONNECT; BIND / UDP ASSOCIATE explicitly return 0x07 command not supported.
  • Remote service connection refusal and SSH layer failures are two different error codes (target-refused / transport), and the panel prompts separately.

Typical Usage: Try Without a Server

The repository includes a local fake remote scripts/dev-remote.mjs: exec runs the local sh (command typos and quote mistakes are exposed immediately), SFTP is read-only (cannot delete real files), but the forwarding capability is real: direct-tcpip connects out from the local machine, and tcpip-forward opens a listener on the local machine to push the channel back:

node scripts/dev-remote.mjs /path/to/project 2202
# Create a new host 127.0.0.1:2202 in the panel, fill in any user and password

Once created, you can use it to truly run the three types of forwarding, remote search, file transfer resume, and multi-hop jump chains.

Use Cases and Considerations

Suitable scenarios: developers who run the DSH agent locally but need to operate on another server (viewing logs, modifying remote configs, starting remote dev servers, syncing local changes up, or temporarily tunneling for the server). People who want these operations to remain in the DSH sidebar and do not want to open a separate SSH client.

Points to note:

  • The plugin runs within the current dsh process, executing with that process’s permissions. The host bundle (lib/server.mjs, about 1 MiB, with ssh2 / ws / chokidar all inlined) is loaded directly by the plugin. Before installing, you should check the source code and license (MIT) before putting it into use.
  • Version 0.x sync is default one-way local → remote, with local as truth; conflicts are not auto-resolved. Understand this semantics before using bidirectional mappings.
  • -R exposes your local port to the network where the server is located; binding to 0.0.0.0 should be done with caution.
  • Passwords and passphrases are only stored in the browser’s memory; the plugin is not responsible for remembering passwords (version 0.7.0 explicitly states “not yet done”), requiring re-entry or going back to the connection page to ask once for each connection.
  • Ecosystem background: DSH’s philosophy is “everything is a plugin,” and the community directory site (like SkillHub) is an independent site with no official affiliation to DeepSeek / Synthflow; this plugin is a third-party open-source project and does not represent the official app store listing.

Conclusion

The value of this plugin lies in converging “local workspace + remote machine” into the same DSH interface: files, terminal, sync, and forwarding each take up a screen, rather than four separate tools. The capabilities are also clearly defined: it does not serve as a generic SSH client, does not replace DSH’s execution world, does not automatically resolve conflicts, and does not remember passwords.

Directory Page: https://www.skillhub.cn/plugins/LinHao100076/dsh-ssh-remote

GitHub: https://github.com/LinHao100076/dsh-ssh-remote

羽毛球分组比赛记分
小程序二维码

欢迎使用《羽毛球分组比赛记分》微信小程序

Xiaoye