Skip to content

dsh-ssh

Verified

@logictan/dsh-ssh · v0.1.3 · MIT · Web UI

DSH SSH: run commands and move files on configured SSH hosts from the agent, addressing hosts by alias, with passwords kept in the credential store. DSH SSH 子插件。

Install

dsh plugin add @logictan/dsh-ssh

Confirm the layer applied with dsh --profile default --dump-config — see the install guide.

Source

Published to npm without a public repository. Inspect the package contents before installing.

Tags

Readme

@logictan/dsh-ssh

SSH for the DSH agent: five tools that run commands, read files and move files on hosts the user configured, with the password kept out of the conversation.

Why not the ssh binary

The secret-safe-ssh skill this plugin replaces spawned the real ssh client and fed the password through SSH_ASKPASS plus a Python helper. That route needs OpenSSH, python and a POSIX shell on the machine running DSH — so it never worked on Windows, and every call had to re-derive host facts from a YAML inventory instead of from the plugin's own configuration.

This plugin speaks the protocol in-process with ssh2, which is pure JavaScript. The same code path runs on macOS, Linux and Windows, and the password never reaches a command line, an environment variable or a temporary file — it goes from the credential store into the connection options and nowhere else.

ssh2 is bundled into lib/index.js rather than declared as a dependency, so this package installs with nothing to compile and no build script to approve. That is not an optimisation: ssh2 ships an install script that pnpm refuses to run unless the user's profile has allow-listed it, and its refusal fails the whole install (ERR_PNPM_IGNORED_BUILDS). The script only tries to compile an optional native crypto binding — the plugin runs on the pure-JS fallback, verified for exec and SFTP — so depending on ssh2 would make every install fail on behalf of a binding nobody needs. Details in build.mjs.

Settings

Settings → Plugins → dsh-ssh:

Field Meaning
alias the handle the agent uses (ssh_exec({ alias: "nas", … })); letters, digits, ., -, _
host hostname or address
port defaults to 22
username login user
password not a config field — a credential per host; see below

Each host's password lives in $DSH_HOME/.credentials.yaml as SSH_PASSWORD_<ALIAS> (the alias upper-cased, every other character folded to _), entered in the same card and never read back. Storing it as a config field would defeat that: a config value is redacted from settings reads, but the plaintext still sits in cordis.patch.yml, which the config sync exports verbatim.

Two aliases that fold to the same reference (a-b and a.b) are rejected: they would silently share one password.

Tools

Tool Purpose
ssh_list the configured hosts, their alias/host/port/username, and whether a password is stored. Opens no connection.
ssh_exec run one shell command; returns exit code, stdout and stderr
ssh_read read a remote text file as a line-numbered window, the same shape as the local read tool
ssh_upload copy one local file to the host over SFTP
ssh_download copy one file from the host over SFTP

Every tool takes an alias, never a hostname: the configured table is the whole set of reachable machines.

ssh_exec is not interactive — no TTY, so a command that waits for input (sudo's password prompt, an editor, read, a pager) blocks until its timeout and is then aborted. Each call opens its own connection and closes it, so no state (working directory, environment, background jobs) survives between calls.

ssh_exec takes workdir instead of a cd /x && … prefix, matching the local bash tool. The SSH exec request has no working-directory field, so the plugin puts a quoted cd in front of the command and exits non-zero if the directory does not exist — running the command somewhere else and succeeding would be worse than failing.

ssh_read exists so that reading a remote file does not mean cat through ssh_exec: it returns the same <path>/<content> envelope, 1-based line numbers, and offset/limit paging the local read tool uses. Only the head of the file is transferred (512 KiB), so a larger file gets a footer saying the later lines were not scanned rather than a false "end of file".

The timeout budget is reported in the result header (timeout_budget: 120s), never after the output — a trailing timeout: 300s reads as "this command timed out", which is exactly what it does not mean. A multi-line command is echoed by its first line plus a line count, so a heredoc does not bury the output it produced.

Host key verification

ssh2 accepts any host key unless told otherwise, which with password authentication means handing the password to whoever answers on the address. The plugin records the first key it sees per host:port in $DSH_HOME/dsh-ssh/known-hosts.json and refuses every later connection that presents a different one — OpenSSH's accept-new policy.

This cannot protect the very first connection; it protects every one after it, and it turns a rebuilt host (or an impostor) into an error naming both fingerprints instead of a leaked password. The file is deliberately per device and not synced: trust is a local judgement.

After a legitimate rebuild, delete that address's entry from the file and reconnect.

Scope

Password authentication only, one hop, no jump hosts, no key files, no port forwarding, no sudo handling. A pooled connection is out of scope too: each operation connects, runs, and disconnects.