DeepSeek Harness plugin

dsh-ssh

SSH remote-execution plugin: ProxyJump chain, SFTP filesystem, subprocess and PTY over ssh2.

Jump to install

Source facts

Repository
UynajGI/dsh-ssh
Latest update
Aug 16, 2026
Category
Tools & Capabilities
GitHub stars
7
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/UynajGI/dsh-ssh
Plugin: dsh-ssh
Author: UynajGI

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

dsh-ssh

English | 中文

<p align="center"> <img src="https://img.shields.io/npm/v/dsh-ssh" alt="npm version"> <img src="https://img.shields.io/npm/l/dsh-ssh" alt="license"> <img src="https://img.shields.io/badge/node-%3E%3D22-339933" alt="node version"> <img src="https://img.shields.io/github/actions/workflow/status/UynajGI/dsh-ssh/ci.yml?label=CI" alt="CI status"> <img src="https://img.shields.io/github/stars/UynajGI/dsh-ssh" alt="GitHub stars"> <img src="https://img.shields.io/badge/dsh-plugin-2ea44f" alt="dsh-plugin"> </p>

SSH remote-execution plugin for DeepSeek Harness. Moves Bash, file tools, PTY terminals, and LSP onto a remote host over a single SSH connection — with multi-hop ProxyJump chains, SFTP upload/download, and full auth coverage. Built on ssh2.

> First (and as of 2026-08, only) SSH remote-development plugin in the dsh-plugin ecosystem. Verified end-to-end against a real two-hop jump environment with key auth.

Architecture: local brain, remote hands

Your machine (deepseek-harness)                      Remote host
┌────────────────────────────────────┐    SSH    ┌──────────────────────┐
│ agent loop (orchestration, memory) │◄──────────►│ bash / command exec  │
│ LLM API calls (direct, no egress)  │   exec    │ filesystem (SFTP)    │
│ credentials / config / sessions    │   pty     │ PTY terminals        │
│ ctx.subprocess → dsh-ssh           │   sftp    │ LSP / git / builds   │
│ ctx.fs → dsh-ssh                   │           │                      │
└────────────────────────────────────┘           └──────────────────────┘

The harness does not need to be installed remotely. dsh-ssh implements remote providers for two of the harness's capability seams — ctx.subprocess (remote processes) and ctx.fs (remote files). Every tool built on those seams (bash, file tools, terminals, LSP, subagent processes) switches to the remote host with zero changes: the model thinks locally, commands run remotely, results stream back into the local model context.

Install

npm i dsh-ssh

Quick start (cordis.yml)

One row mounts everything — the shared connection owner plus both remote providers:

- id: ssh-remote
  name: dsh-ssh
  config:
    host: server.example.com  # target host (required; a ~/.ssh/config alias like prod works too)
    port: 22
    username: root            # required
    privateKey: ~/.ssh/id_ed25519   # identity-file path, or PEM content
    # password: 'xxx'               # password auth (mutually usable with privateKey)
    # agent: 'pageant'              # Windows Pageant; Unix: SSH_AUTH_SOCK path
    cwd: /root/workspace           # remote working directory (required, absolute POSIX path)
    # --- ProxyJump chain (optional; first hop from local, last hop to target) ---
    jump:
      - host: bastion.example.com
        # port: 22             # defaults to the target's
        # username: ubuntu     # defaults to the target's
        privateKey: ~/.ssh/id_ed25519
      # - host: second-hop ...
    # --- Connection & security ---
    readyTimeout: 45000        # ~ ConnectTimeout (ms, default 45s; relayed links handshake slowly)
    keepaliveInterval: 0       # ~ ServerAliveInterval (0 disables)
    keepaliveCountMax: 3       # ~ ServerAliveCountMax
    strictHostKeyChecking: false   # verify the host key when true
    knownHosts:                    # required when strictHostKeyChecking: true
      - 'SHA256:xxxxxxxx...'

The aggregate row is equivalent to three subpath rows — mount them separately only when a deployment composes providers individually:

- id: ssh
  name: dsh-ssh/ssh            # ctx.ssh connection owner (config above)
- id: subprocess-ssh
  name: dsh-ssh/subprocess     # ctx.subprocess remote provider
- id: fs-ssh
  name: dsh-ssh/fs             # ctx.fs remote provider (SFTP)

Add-workspace over SSH (Web GUI)

The Web surface's Add workspace flow (the conversation hero picker and the sidebar workspace browser) is taken over by the dsh-ssh client UI, laid out as a connection sidebar beside a directory browser (VS Code Remote Explorer style): the sidebar lists ~/.ssh/config hosts, saved connections, and the local entry; the right pane browses whichever side is active. Local listing rides the ctx.directoryPicker browse capability; connection management and remote listing ride dsh-ssh's own /dsh-ssh RPC channel. While browsing the local side, the toolbar also offers a system chooser button (the local.pickNative endpoint, reusing the host's OS-native folder dialog) — the directory picked in the popup becomes the workspace directly, no level-by-level walking required.

~/.ssh/config hosts in the sidebar (config.hosts)

The「SSH 配置主机」sidebar section is driven by the config.hosts endpoint: every opening re-reads the host machine's ~/.ssh/config and lists its exact Host aliases (wildcard patterns such as *.example.com stay hidden), each with the resolved user@host:port, IdentityFile presence, and ProxyJump presence:

  • Clicking an alias resolves its full config (username, port, identity, jump

chain), registers it silently, and drops you straight into that host's directory browser — no form (VS Code Remote-SSH style). Registered aliases get an「已添加」badge and simply switch on click.

  • An alias without a User skips registration and opens a prefilled form

(port / identity / jumps already filled); only the username is missing.

  • An alias without an IdentityFile can be registered, but the connection

fails at auth — the right pane translates All configured authentication methods failed into a readable hint with a「补全认证」button that opens the prefilled form.

The「新建连接」form is alias-first as well: type a ~/.ssh/config alias and blur or paste auto-resolves and prefills (the manual「识别 ssh 配置」button remains the loud fallback); a successful resolve shows a one-line summary (alias → user@host:port, identity path, jump chain) inside the form.

A patch layer's name is a match guard rather than a replacement, so the Web bundle's @deepseek-ai/dsh-host-directory-picker-auto row must be disabled by id (its dynamically mounted in-app picker disappears with it) and the SSH backend inserted under its own id. In the Web profile ($DSH_HOME/profiles/web/cordis.patch.yml):

# Disable the boot-resolved picker (its dynamic entries go with it).
- id: directory-picker
  name: '@deepseek-ai/dsh-host-directory-picker-auto'
  disabled: true

- insert:
    - id: ssh-remote
      name: dsh-ssh
      config: { ...same config as the quick start... }

    # The local-directory browse backend for the client directoryFlow slots.
    - id: directory-picker-ssh
      name: dsh-ssh/picker
      config:
        maxEntries: 1000

    # The connection registry + /dsh-ssh RPC (persistence, ~/.ssh/config
    # awareness, remote directory browsing).
    - id: ssh-web-channel
      name: dsh-ssh/web
      config:
        maxEntries: 1000

Picking a remote directory first asks /dsh-ssh session.route for a LOCAL placeholder directory (<DSH_HOME>/dsh-ssh-routes/<connectionId>/<remotePath>, created host-side) and creates the session with it:

const { cwd } = await rpc('session.route', { id: connectionId, path: remotePath })
ctx.sessions.create({ cwd })

The detour exists because the host's session service mkdirs the project directory through node:fs — an ssh://… cwd cannot pass that check, while mkdir succeeds silently for an existing directory. ctx.subprocess and ctx.fs recognize both the ssh://<id>/<path> spelling and the local placeholder prefix, routing that session's bash / file / terminal operations onto the registered connection's directory. Remote sessions do not enter the DSH local workspace registry (see「Known limitations」); deleting a connection also removes its placeholder tree.

Picker configuration (dsh-ssh/picker)

FieldTypeDefaultDescription
maxEntriesnumber1000Complete-result bound for one listed level (hidden rows count; truncated flags a cut)
remoteLabelstringRetained field: the client flow no longer uses a pinned entry; remote entries live in the left connection sidebar

dsh-ssh/picker now serves only the ctx.directoryPicker browse backend (local directories keep working on Windows hosts; POSIX absolute paths go to the aggregate SSH connection). The client UI's remote connection list and remote directory browsing ride the dsh-ssh/web RPC channel instead.

Configuration reference (dsh-ssh/ssh)

FieldTypeDefaultDescription
hoststringTarget hostname or address (required)
portnumber22Target SSH port
usernamestringRemote login user (required)
passwordstringPassword auth
privateKeystringPEM key content or local identity-file path
passphrasestringPassphrase for an encrypted key
agentstringssh-agent socket path or pageant
jumpJumpConfig[][]ProxyJump chain; per-hop port/user/auth overrides
cwdstringRemote working directory (required, absolute POSIX path)
readyTimeoutnumber45000Connection timeout (ms)
keepaliveIntervalnumber0SSH keepalive interval (ms)
keepaliveCountMaxnumber3Keepalive failure threshold
strictHostKeyCheckingbooleanfalseVerify the host key against knownHosts
knownHostsstring[][]Trusted fingerprints (SHA256:…) or raw base64 public keys

OpenSSH ~/.ssh/config mapping

OpenSSH directivedsh-ssh field
HostName / Port / Userhost / port / username
IdentityFile / IdentitiesOnlyprivateKey (path or PEM)
PasswordAuthenticationpassword
ForwardAgentagent
ProxyJump (comma-separated hops)jump array (per-hop)
ConnectTimeoutreadyTimeout
ServerAliveInterval / ServerAliveCountMaxkeepaliveInterval / keepaliveCountMax
StrictHostKeyChecking + UserKnownHostsFilestrictHostKeyChecking + knownHosts
RemoteCommand / RequestTTYsee spawnTerminal (PTY is consumer-requested)

Capabilities

CapabilityImplementation
ProxyJump chainsjump array, multi-hop (direct-tcpip, equivalent to OpenSSH ProxyJump), independent auth per hop
Authpassword, private key (PEM or path), passphrase, ssh-agent / Pageant; with none configured, falls back to the ~/.ssh default identities (id_ed25519 / id_ecdsa / id_rsa, mirroring OpenSSH)
Upload (local → remote)SFTP atomic write (same-dir temp file + rename, mode preserved)
Download (remote → local)full fs provider: read / streamText (streaming decode) / readBytes (bounded) / listDir / stat / lstat
Remote commandssubprocess provider: collect (bounded tail + local spill file), pipe, inherit, batch stdin
Interactive terminalsPTY (spawnTerminal), I/O plus TERM→KILL cleanup
Add-workspace GUIdsh-ssh/picker: the directory-picker seam's browse backend; the client UI is a connection sidebar (~/.ssh/config one-click hosts + saved connections + local) beside the directory browser
Environment isolationremote login env scrubbed (DSH_* and credential-shaped names removed) + explicit overrides, launched via env -i
Concurrency safetyfs writes serialized per target key (no interleaved writes)
Host verificationstrictHostKeyChecking + knownHosts (SHA256 fingerprints or raw keys)

Performance

  • Connection reuse — all three providers share one SSH connection (jump chain included); the SFTP channel opens lazily, is reused, and rebuilds itself after disconnects.
  • Environment cache — the remote login environment is read once per connection (env -0), not per spawn.
  • Local spill — collect-mode output keeps an in-memory tail plus a local spill file, same semantics as the official local provider.
  • No polling — one exec channel per command (cd && exec env -i -- …); no polling or intermediate state files.

Reliability

  • Exit facts are authoritative — exit code / signal come from the SSH channel close event.
  • UTF-8 safe — exec output is buffered and decoded once; SSH chunking cannot corrupt multi-byte characters.
  • Fail loud — connection, auth, jump, and SFTP failures surface with readable messages.
  • Teardown — plugin disposal terminates active processes/terminals and closes the connection; staging dirs and spill files are cleaned on failure.

Troubleshooting

SymptomCause & fix
All configured authentication methods failedWrong auth config: check username / privateKey path / passphrase; key permissions too open (chmod 600)
Cannot read private keyprivateKey is neither PEM content nor an existing file
Jump connection timeoutCheck hop reachability and readyTimeout; verify the hop's user/auth independently
Host key verification failedstrictHostKeyChecking: true without a matching knownHosts entry; collect the fingerprint with ssh-keyscan
exec exits 127Remote command not found; check the remote PATH (the scrubbed env keeps it)
Write fails with FS_NOT_OBSERVEDFile exists and createIfAbsent was used (overwrite protection, not a bug)

Known limitations

  • Remote pid invisible — SSH channels do not expose the remote pid; SubprocessHandle.pid is always -1.
  • Termination is not tree-scopedterminate signals the remote direct process (SIGTERM → grace → SIGKILL); descendants are not guaranteed to die (inherent to the SSH protocol, unlike the local provider's process groups).
  • No foreground process groupinspectForeground returns undefined and signalForeground throws (the SSH channel cannot resolve a remote foreground group).
  • No reconnection — a dropped connection requires a plugin restart.
  • Remote directories land as sessions — picking a remote directory opens a session through a session.route local placeholder (<DSH_HOME>/dsh-ssh-routes/<id>/<path>; the session list shows that cwd); no record is created in the DSH local workspace registry (dsh-workspace still only accepts local fs.realpath directories).
  • Picker is remote-only on POSIX hosts — every absolute path is a remote path there, so the local filesystem cannot share the picker's vocabulary (Windows hosts keep both worlds via drive/UNC routing).
  • Text-only streamingstreamText rejects binary files with FS_NOT_TEXT (same as the official provider).

Development

npm i
npm run typecheck
npm run build       # emits lib/ — the compiled payload the harness loader imports
  • Git hooks (husky): pre-commit typechecks; commit-msg enforces Conventional Commits; pre-push rejects a version tag that does not match package.json.
  • CI (GitHub Actions): typecheck + publishable-payload check on every push/PR.
  • Release (GitHub Actions): push a version tag to publish to npm and draft a GitHub Release:
npm version patch -m "chore(release): v%s"   # bumps package.json + commits + tags
git push origin main && git push origin --tags

The tag must match the version field in package.json (both hooks and the release workflow enforce it). Publishing uses the NPM_TOKEN repository secret (an npm Automation token — it bypasses 2FA for CI).

License

MIT