@magnaboy/cli-ssh
v0.0.2
Published
Unattended SSH: batch-mode remote scripts, POSIX quoting and destination validation for Node.js CLIs.
Readme
@magnaboy/cli-ssh
SSH that is safe to run unattended: batch-mode options that fail instead of hanging, remote scripts fed over stdin, and the quoting and validation that keep a hostname or a path from becoming a command.
Install
npm i @magnaboy/cli-sshRequires Node 25+ and ESM. Calls take a ProcessRunner from @magnaboy/cli-core.
import { quote, SshClient } from '@magnaboy/cli-ssh';Unattended by default
import { requireSsh, SshClient, SSH_BATCH_OPTIONS } from '@magnaboy/cli-ssh';
const ssh = new SshClient({ runner, host: '[email protected]', executable: await requireSsh(runner) });SSH_BATCH_OPTIONS is what makes a script safe to leave alone:
BatchMode=yesturns a password or passphrase prompt into an immediate failure, rather than a script that waits forever on a terminal nobody is watching.StrictHostKeyChecking=yesrefuses an unknown or changed host key instead of trusting it.ConnectTimeoutand the keepalives bound how long a dropped connection looks like a slow command.
Pass your own options to replace them.
Remote scripts
await ssh.script(`set -eu
systemctl restart cx.service
systemctl is-active cx.service
`, { admin: true, directory: '/opt/cx' });The script travels on stdin, not as an argument. Nothing re-parses it — not the local shell, not the remote login shell — and its length is not bounded by the argument limit.
admin: true escalates with sudo -n only when not already root. The -n keeps BatchMode's
promise: a sudo password prompt fails rather than hangs.
assertPosixScript refuses a script with no set -e, which otherwise runs on past a failed line —
the failure mode where a deploy continues after its build step died.
One command, quoted
await ssh.command(['systemctl', 'restart', serviceName]);
const state = await ssh.capture(['systemctl', 'is-active', serviceName]);
await ssh.upload('./build.tar', '/opt/cx/build.tar');Every argument is quoted for the remote shell, so a service name containing ; restarts nothing
rather than running something.
Quoting and validation
import { quote, remotePath, sshHost } from '@magnaboy/cli-ssh/shell';quote wraps a value in single quotes, which suspend every expansion, closing and reopening around
an embedded quote. A remote command is assembled as text by definition — the local argument array
does not survive the hop — so this is the boundary where a path with a space, a $ or a ; either
stays data or becomes another command. It is tested by round-tripping through a real bash.
sshHost refuses anything that is not an alias or user@hostname. The destination is the one part
of an SSH invocation that cannot be quoted: a value starting with - is read by ssh as an option,
which is how -oProxyCommand=... turns a hostname into local command execution.
remotePath requires an absolute path and rejects .. traversal.
