# mscdex/ssh2: a pure JavaScript SSH2 client and server for Node.js

> ssh2 implements the SSH2 protocol in JavaScript, so a Node process can open exec channels, shells, SFTP sessions and port forwards without shelling out to the OpenSSH binary. The library covers both ends of the connection, which is unusual and also the source of its sharpest constraints.

**mscdex/ssh2** — SSH2 client and server modules written in pure JavaScript for node.js

- Repository: https://github.com/mscdex/ssh2
- Stars: 5,825 · Forks: 737
- Language: JavaScript
- License: MIT
- Published: 2026-09-22 · Updated: 2026-09-22 · Language: en
- Canonical page: https://hysenlabs.com/projects/mscdex-ssh2

## The gap ssh2 fills for Node.js services

Most Node code that talks SSH does it by spawning the ssh binary and parsing stdout. That works until it does not: exit codes get tangled with shell quoting, passwords cannot be typed into a non-interactive child process, and there is no way to accept an incoming SSH connection at all. ssh2 replaces the subprocess with the protocol itself. The package description in package.json is blunt about the scope: SSH2 client and server modules written in pure JavaScript for node.js.

The audience is narrow and specific. It is for backend engineers who need to run a command on a remote host from inside an application, move files over SFTP as part of a pipeline, or tunnel a TCP connection through a jump host. It is also for people building the other side: an SFTP endpoint, a chat server over SSH, or a test fixture that speaks SSH so a client can be exercised without a real daemon. The repository ships examples/server-chat.js and examples/sftp-server-download-only.js for exactly those two server shapes.

What it is not is a terminal emulator or a replacement for the ssh command in your daily work. There is no config file, no ~/.ssh/config parsing, no known_hosts file, and no interactive prompt for a passphrase. Every one of those has to be supplied by the calling code.

## How the client, channels and forwarding fit together

The architecture follows the SSH2 protocol rather than abstracting it away. A Client instance represents one transport connection. You attach listeners, then call connect with a configuration object holding host, port, username and either a password or a privateKey. The ready event fires once authentication succeeds. From that single connection you open channels: exec for a command, shell for an interactive session, sftp for the file subsystem, and the forward methods for tunnels.

Each channel is a duplex stream. The exec example in the README shows the shape clearly: conn.exec('uptime', (err, stream) => ...) hands back a stream with data and stderr events and a close event carrying the exit code and signal. That is the whole contract. There is no promise wrapper and no queue; if you open five channels, you manage five streams and their error paths.

Forwarding works in both directions. forwardOut opens a tunnel from the local side to a host and port reachable from the SSH server, which is how the README sends a raw HTTP request to port 80 on the remote machine. forwardIn asks the server to listen on a port and delivers incoming connections to the client through a tcp connection event, where the handler calls accept or reject. The callback-based accept and reject pair is the detail people miss: the server will not proceed until your handler answers.

The server side mirrors this. A Server emits connection events, each carrying a Client-like connection object with its own authentication events and session handling, plus the same channel and forwarding surface. Because both ends are in the same package, the same channel abstractions apply whether you are the caller or the callee.

## Installing ssh2 and running a first remote command

The README gives one installation line. The package requires Node.js v16.0.0 or newer, matching the engines field in package.json, and it declares asn1 and bcrypt-pbkdf as regular dependencies.

```bash
npm install ssh2
```

The install script runs node install.js, which is also exposed as the rebuild script. That script attempts to build the optional cpu-features addon, which the README says is used to help generate an optimal default cipher list. On a platform where the native build fails, the package is designed to continue without it, so a failed optional build is not by itself a reason to abandon the install.

The smallest useful program is the exec example from the README, trimmed to a single command. It reads a private key from disk and runs uptime on the remote host.

```js
const { readFileSync } = require('fs');
const { Client } = require('ssh2');

const conn = new Client();
conn.on('ready', () => {
  conn.exec('uptime', (err, stream) => {
    if (err) throw err;
    stream.on('close', (code, signal) => {
      conn.end();
    }).on('data', (data) => {
      console.log('STDOUT: ' + data);
    }).stderr.on('data', (data) => {
      console.log('STDERR: ' + data);
    });
  });
}).connect({
  host: '192.168.100.100',
  port: 22,
  username: 'frylock',
  privateKey: readFileSync('/path/to/my/key')
});
```

What you should see is the ready callback firing, then the uptime output arriving on the data event, then close with an exit code. The README's own sample output shows the stream close line reporting code 0 and signal undefined. If ready never fires, the problem is almost always authentication or host reachability, not the channel code.

## Host key verification is your job, not the library's

This is the limitation that matters most in production. The connect configuration accepts host, port, username and credentials, and the README's examples pass those and nothing else. There is no known_hosts file to consult and no default policy that rejects an unknown server key. The library exposes the host key information so a caller can check it, but the check itself is application code.

That means a service built on the examples as written will connect to whatever answers on the host and port. For a script talking to a machine on a private network this is often acceptable. For anything crossing a network you do not control, it is not, and the fix is not a configuration flag. You have to capture the key the server presents, compare it against a value you stored earlier, and abort the connection on a mismatch. The README does not document a rollback or recovery path for that decision, because there is nothing to roll back: the connection either proceeds or you end it.

The same pattern appears in the server examples. Password and public key authentication are both demonstrated, but the policy of which keys are trusted is written by the application. ssh2 gives you the mechanism and stays out of the decision. If you want the decisions made for you, this is the wrong tool.

## Where ssh2 stops and node-ssh or ssh2-promise begins

The callback style is deliberate and it shows. Two packages in the related ecosystem exist mainly to wrap it. node-ssh and ssh2-promise both take the same protocol implementation and put promises and convenience helpers in front of it, so a caller writes an awaited method instead of nesting callbacks and tracking streams by hand.

The difference in approach is worth being precise about. ssh2 gives you the channel as a stream and leaves lifecycle management to you, which is what you want when you are piping data, handling backpressure, or multiplexing many channels over one connection. A promise wrapper gives you a resolved value per call, which is what you want when the operation is short and sequential: run this command, read the result, close.

There is a cost to the wrapper. Anything the wrapper does not expose, you reach back through to the underlying client anyway, and now you are debugging two layers. If your usage is a handful of sequential commands, a wrapper will read better. If you are building the server side, or forwarding ports, or holding one connection open for hundreds of channels, the wrapper adds nothing and the raw streams are the clearer interface.

## Maintenance, licensing and the cost of upgrading

The repository is not archived, and the last push was on 2026-08-20. package.json lists version 1.17.0. The licence is MIT, declared in package.json and shipped as the LICENSE file at the repository root. MIT is permissive: it allows use in closed-source products provided the copyright notice and permission notice are retained. That is a description of the licence text, not legal advice, and anyone embedding the library in a distributed product should read the LICENSE file rather than this paragraph.

The upgrade cost is dominated by the Node.js floor. The engines field requires >=16.0.0, so a project still on an older runtime cannot install it without changing runtimes first. Beyond that, the dependency set is small: asn1 and bcrypt-pbkdf are the only runtime dependencies, and cpu-features and nan are optional. A small dependency tree is the main reason this package is easy to keep current; there is very little transitive surface to audit.

The optional native addon is the one part of the install that can behave differently across machines. install.js attempts the build and the package continues without it, which means two developers can end up with different default cipher lists depending on whether the addon compiled. If cipher negotiation ever differs between environments, that is the first place to look.

## Conclusion

Adopt ssh2 when the SSH session has to live inside a Node process: a deploy script that runs a remote command, an SFTP upload step in a build pipeline, a test harness that needs a throwaway SSH server. Do not adopt it when the job is a general interactive terminal for humans, or when you need a configuration file, host key management and known_hosts handling that OpenSSH already provides. Before committing, verify three things in your own environment: that your Node version satisfies the engines field of >=16.0.0, that the optional cpu-features build succeeds or fails cleanly on your platform, and that your host key verification logic is written against the connect configuration you actually pass, because the library will not do it for you.

## FAQ

### What is the purpose of SSH?

The README does not describe the purpose of the SSH protocol in general. It presents ssh2 as an SSH2 client and server implementation in pure JavaScript for Node.js, used for remote command execution, interactive shells, SFTP and port forwarding.

### When did SSH2 come out?

The README gives no release history for the SSH2 protocol. For this package, package.json lists version 1.17.0, and no recent releases were retrieved.

### What protocol does SSH run on?

The README does not state which transport protocol SSH runs over. It only describes ssh2 as implementing SSH2 in pure JavaScript for Node.js, with development and testing done against OpenSSH 8.7.

## Sources

- [Issues](https://github.com/mscdex/ssh2/issues)
- [License: MIT](https://github.com/mscdex/ssh2/blob/master/LICENSE)
- [mscdex/ssh2 on GitHub](https://github.com/mscdex/ssh2)
- [README](https://github.com/mscdex/ssh2/blob/master/README.md)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/mscdex-ssh2
