This server runs commands on live machines. A defect here is not a crashed page — it is a command on the wrong server, a truncated file, or a password in a log. The list below is what the project promises, and how to tell us when a promise breaks.
| Version | Supported |
|---|---|
| 2.x | Yes |
| 1.x | No — the ssh2 backend it was built on is gone |
Use GitHub's private reporting: Report a vulnerability. It stays between you and the maintainer until a fix ships.
Please do not open a public issue for anything that lets someone run a command they should not, read a secret they should not, or destroy data without the guard firing.
What helps most, in order:
- The exact tool call and profile shape that triggers it (secrets removed).
- What you expected and what happened instead.
- Your
ssh -Vand OS — behaviour differs between OpenSSH versions and between BusyBox and coreutils servers, and a report without them may not reproduce.
Expect a first reply within a few days.
It protects against the slip, not against you. Two guards run before a command
leaves the machine: one refuses irreversible destruction (wiping a disk, dropping a
database, crontab -r), one refuses a recursive delete aimed at the filesystem root,
a home directory or a system tree, symlinks included. Both step aside for an explicit
# CONFIRMED-DESTRUCTIVE marker. Neither is a policy engine, and ssh_exec is not a
sandbox: it runs the command you give it, && and ; included, because that is what
the tool is for.
Secrets stay out of argv and out of logs. A password reaches ssh through an
askpass helper that reads one environment variable of one child process, so ps does
not show it. It is never written to disk and is masked in this server's logs. Secrets
belong in a secretsFile that must be chmod 600 — see
docs/security.md.
Every call names its server. There is no default profile. A call without a profile is refused rather than sent to whichever entry happens to come first in the file.
A transfer never destroys the only intact copy. Data is written next to the target
under a temporary name and moved into place by a single rename. When verification is
impossible — no sha256sum on the server, output truncated, the guard killed the
command — the answer says "could not verify" instead of claiming a mismatch, because a
false mismatch would make the installer delete what it just delivered.
A refusal never teaches you to walk around your own rule. When a tool cannot do
something, its answer names ssh_exec as the way through — but not when the refusal came
from your own configuration. A path outside pathSecurity is refused without that hint,
because a fence you set on purpose is not a limitation to route around. The same holds for
the destructive-command guards: they refuse, and the answer names the # CONFIRMED-DESTRUCTIVE
marker that you have to write yourself, never a way to get the same effect unnoticed.
What is out of scope. Whoever can call this MCP server can reach every machine in
the profiles file with the rights of those accounts. The server does not authenticate
its caller — that is the MCP client's job. Restrict what an account may do on the
server side (a non-root user with narrow sudo), and use pathSecurity to fence off the
paths file tools may touch.
One runtime dependency: @modelcontextprotocol/sdk. Nothing else is bundled — the SSH
work is done by the OpenSSH client already on your machine, so its patches are your
system's, not this package's.