DSH HUB
HomePlugin StorePlugin PacksCommunityRankingsResourcesPublish Guide
Plugin source
Back to catalog

UynajGI /

dsh-ssh

Verified

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

★ 5 Stars0 Forks1 IssuesN/A Community rating0 Confirmed installs
View on GitHub
READMESource: main@9c344efb

dsh-ssh

English | 中文

npm version license node version CI status GitHub stars dsh-plugin

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)

Field Type Default Description
maxEntries number 1000 Complete-result bound for one listed level (hidden rows count; truncated flags a cut)
remoteLabel string — Retained 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)

Field Type Default Description
host string — Target hostname or address (required)
port number 22 Target SSH port
username string — Remote login user (required)
password string — Password auth
privateKey string — PEM key content or local identity-file path
passphrase string — Passphrase for an encrypted key
agent string — ssh-agent socket path or pageant
jump JumpConfig[] [] ProxyJump chain; per-hop port/user/auth overrides
cwd string — Remote working directory (required, absolute POSIX path)
readyTimeout number 45000 Connection timeout (ms)
keepaliveInterval number 0 SSH keepalive interval (ms)
keepaliveCountMax number 3 Keepalive failure threshold
strictHostKeyChecking boolean false Verify the host key against knownHosts
knownHosts string[] [] Trusted fingerprints (SHA256:…) or raw base64 public keys

OpenSSH ~/.ssh/config mapping

OpenSSH directive dsh-ssh field
HostName / Port / User host / port / username
IdentityFile / IdentitiesOnly privateKey (path or PEM)
PasswordAuthentication password
ForwardAgent agent
ProxyJump (comma-separated hops) jump array (per-hop)
ConnectTimeout readyTimeout
ServerAliveInterval / ServerAliveCountMax keepaliveInterval / keepaliveCountMax
StrictHostKeyChecking + UserKnownHostsFile strictHostKeyChecking + knownHosts
RemoteCommand / RequestTTY see spawnTerminal (PTY is consumer-requested)

Capabilities

Capability Implementation
ProxyJump chains jump array, multi-hop (direct-tcpip, equivalent to OpenSSH ProxyJump), independent auth per hop
Auth password, 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 commands subprocess provider: collect (bounded tail + local spill file), pipe, inherit, batch stdin
Interactive terminals PTY (spawnTerminal), I/O plus TERM→KILL cleanup
Add-workspace GUI dsh-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 isolation remote login env scrubbed (DSH_* and credential-shaped names removed) + explicit overrides, launched via env -i
Concurrency safety fs writes serialized per target key (no interleaved writes)
Host verification strictHostKeyChecking + 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

Symptom Cause & fix
All configured authentication methods failed Wrong auth config: check username / privateKey path / passphrase; key permissions too open (chmod 600)
Cannot read private key privateKey is neither PEM content nor an existing file
Jump connection timeout Check hop reachability and readyTimeout; verify the hop's user/auth independently
Host key verification failed strictHostKeyChecking: true without a matching knownHosts entry; collect the fingerprint with ssh-keyscan
exec exits 127 Remote command not found; check the remote PATH (the scrubbed env keeps it)
Write fails with FS_NOT_OBSERVED File 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-scoped — terminate 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 group — inspectForeground 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 streaming — streamText 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

—/ 5

No ratings yet

Verified DSH bundle

Commit 9c344efb9bef

Community comments

No comments yet. Be the first to write one.

DSH HUB

A community index for DSH plugins. Not an official GitHub or DeepSeek AI product.

CommunityResourcesAPIAbout