ssh2shell
ssh2shell uses ssh2 to open an SSH shell
session, run a list of commands in order, and process the responses.
It is built for scripted, non-interactive work: deployments, audits, data collection, and chained or tunnelled connections where you need the output of one command to decide what to do next.
- Runs commands sequentially in a single shell session.
- Detects the command prompt to know when a command has finished.
- Handles
sudo,su,keyboard-interactive, and private-key passphrases. - Connects to multiple primary hosts or tunnels through intermediate hosts.
- Emits events so you can react to output as it arrives.
Contents
- Why ssh2shell
- Installation
- Quick start
- Core concepts
- Configuration reference
- Recipes
- Multiple hosts
- Events and the API
- Advanced usage
- Troubleshooting
- Security
- Development
- License
Why ssh2shell
ssh2 gives you a connection and a stream. ssh2shell adds the part most
scripts need: a state machine that writes one command at a time, waits for the
prompt, captures the response, and moves on. That makes it a good fit when:
- You have a fixed list of commands to run on one or more hosts.
- You need the full transcript as a string (
sessionText). - You need to answer a prompt (
sudo,su,[y/N]?) without a human. - You need to reach a host through a jump host.
It is not an interactive terminal library. There is no terminal UI, and commands are matched by prompt patterns rather than by a full PTY emulator.
Installation
npm install ssh2shell
Requires Node.js 18 or newer.
Quick start
const fs = require('fs');
const SSH2Shell = require('ssh2shell');
const host = {
server: {
host: '192.168.0.10',
userName: 'deploy',
privateKey: fs.readFileSync(process.env.PRIVATE_KEY_PATH),
},
commands: ['echo $(pwd)', 'ls -l'],
};
const SSH = new SSH2Shell(host);
SSH.connect((sessionText) => {
console.log(sessionText);
});
The callback receives the complete session text, including the output of every
command. If you prefer events, listen for end instead:
SSH.on('end', (sessionText, sshObj) => {
console.log(`${sshObj.server.host}:\n${sessionText}`);
});
SSH.connect();
See test/live/simple.js for a runnable version that
writes the session to a log file.
Core concepts
The host object. A host object has two required parts: server (how to
connect) and commands (what to run). Everything else is optional.
const host = {
server: { /* connection settings */ },
commands: ['pwd', 'ls -l'],
};
One shell, many commands. Commands are written to a single shell session in
order. After each write, ssh2shell waits until the output matches
standardPrompt (by default a line ending in >, $, %, or #) before it
sends the next command. This is why the prompt matters: if the host uses a
different prompt, detection fails and the command times out.
sessionText. Each host accumulates its own output in sshObj.sessionText.
When all hosts finish, connect(callback) passes the combined text.
Primary hosts vs. tunnelling. These are different features:
| Feature | Config | How it connects | Use it when |
|---|---|---|---|
| Multiple primary hosts | new SSH2Shell([host1, host2]) |
A new ssh2 connection per host, one after another |
You have several independent servers |
| Tunnelling | host1.hosts = [host2] |
An ssh command run inside the current session |
You must reach a host through a jump host |
Notifications. Two command entries are handled locally and are not sent to the host:
"msg:Text"emits amsgevent."`Text`"addsTexttosessionTextonly.
Configuration reference
The defaults below are the values used when a property is omitted. Optional properties never need to be included just to keep a default.
server
server is passed to ssh2.Client.connect(), so any option that ssh2 accepts
can be used. The properties below are the ones ssh2shell documents or reads
directly.
| Property | Type | Default | Description |
|---|---|---|---|
host |
string | — | Required. Host name or IP address. |
port |
number | string | 22 |
SSH port. |
userName |
string | — | Required. Login name. username is accepted as an alias. |
password |
string | — | Password authentication. |
privateKey |
Buffer | string | — | Private key contents, usually from fs.readFileSync(). |
passPhrase |
string | — | Private-key passphrase. passphrase is accepted as an alias. |
tryKeyboard |
boolean | false |
Enable keyboard-interactive authentication. |
hostHash |
string | — | Hash algorithm used to verify the server key. Any algorithm from crypto.getHashes() works. sha256 is a good choice. |
hostVerifier |
function | — | (hashedKey, verify) => boolean. Called with the hashed key when hostHash is set. |
algorithms |
object | — | ssh2 cipher, compression, hmac, kex, and server host key settings. |
hashMethod is accepted as a legacy alias for hostHash. It is copied to
hostHash before the connection is opened.
hashKey is not used by ssh2shell or ssh2. Do not set it.
Commands and notifications
| Property | Type | Default | Description |
|---|---|---|---|
commands |
string[] | [] |
Required. Commands to run, in order. |
hosts |
host[] | [] |
Nested hosts to tunnel to. See Tunnelling. |
Command entries can be regular shell commands or notifications:
const host = {
server: { /* ... */ },
commands: [
'msg:Starting deployment',
'`This line is added to sessionText only`',
'cd /srv/app',
'./deploy.sh',
],
};
msg:emits amsgevent with${host}: Note: ${text}. The host prefix is omitted for a single-host session. The defaultmsghandler writes it to the console.- A backtick-wrapped entry is appended to
sessionTextwithout being run. This avoids the command echo you would get fromecho.
See test/live/notifications.js for a runnable
example.
Prompts and response filters
Prompt detection decides when a command is finished. Filters clean the output before it is matched and stored.
| Property | Type | Default | Description |
|---|---|---|---|
standardPrompt |
string | ">$%#" |
Characters that identify a command prompt. Compiled to a regex matching one of these at the end of a line. |
passwordPrompt |
string | ":" |
Text that identifies a password prompt. |
passphrasePrompt |
string | ":" |
Text that identifies a passphrase prompt. |
passPromptText |
string | "Password" |
Text that must appear before the password/passphrase prompt. Shared by both prompt types. |
asciiFilter |
string | "[^\r\n\x20-\x7e]" |
Regex that removes non-standard ASCII. Complete terminal escape sequences are removed first. Tabs are preserved by the compiled default. |
disableASCIIFilter |
boolean | false |
Set to true to keep non-standard ASCII. |
textColorFilter |
string | "\\x1b\\[[0-9;]*m" |
Regex that removes ANSI colour codes. |
disableColorFilter |
boolean | false |
Set to true to keep ANSI colour codes. |
If your host uses a prompt that does not end in >$%#, set standardPrompt to
the characters it does use. For example, a prompt ending in > and a space
still works, but a prompt ending in : will not.
passPromptText is shared by password and passphrase detection. If your host
uses different wording, set it to text common to both prompts.
Filters only affect the text that ssh2shell matches and stores. They do not
change what the host sends.
When ASCII filtering is enabled, complete terminal escape sequences (CSI and
OSC, such as bracketed paste and shell-integration markers) are removed before
asciiFilter runs. Otherwise the ESC byte would be dropped but its printable
payload would remain in the response. Set disableASCIIFilter to keep them.
Timeouts
| Property | Type | Default | Description |
|---|---|---|---|
idleTimeOut |
number | 5000 |
Milliseconds to wait for a command to return to a prompt. |
dataIdleTimeOut |
number | 500 |
Milliseconds of silence used to decide that a response burst has ended. |
idleTimeOut is the command timeout. When it expires, the commandTimeout
event fires. The default handler emits an error of type Command Timeout and
closes the connection.
dataIdleTimeOut is the response-settling delay. It groups stream chunks into
one response before prompt matching. It is not the command timeout.
idleTime is accepted as a legacy alias for idleTimeOut. You can also set
SSH.idleTime or SSH.dataIdleTime on the instance before connect(); a host
value overrides the instance value for that host.
Messages and debug output
| Property | Type | Default | Description |
|---|---|---|---|
connectedMessage |
string | "Connected" |
Emitted as a msg when the connection opens. |
readyMessage |
string | "Ready" |
Emitted as a msg when the shell is ready. |
closedMessage |
string | "Closed" |
Emitted as a msg when the connection closes. |
msg |
function | { send } |
console.log |
Receives every msg event. The legacy { send: fn } shape is accepted. |
verbose |
boolean | false |
Emit the response text after each command. |
debug |
boolean | false |
Emit internal process steps. |
progress |
boolean | true |
Write a stderr liveness indicator while a command is in flight. Set to false to disable. TTY-only; never affects sessionText. |
exposeCredentials |
boolean | false |
Do not redact passwords/passphrases in debug and verbose output. |
showBanner |
boolean | false |
Include the host banner in sessionText. |
enter |
string | "\n" |
End-of-line sequence written after each command. |
streamEncoding |
string | "utf8" |
Encoding applied to the shell stream. |
window |
boolean | true |
First argument to connection.shell(). An explicit false is preserved. |
pty |
boolean | true |
Pseudo-terminal option passed to connection.shell(). An explicit false is preserved. |
Note:
connect,ready, andcloseare not instance events. Use themsgevent and the three message properties above, or listen forend.
Host event handlers
Handlers defined on the host object run only for that host. For most events
they replace the class default. onData runs in addition to the internal
stream handler, and onError runs before the default error handler so it can
observe the error before the connection closes. Handlers added to the instance
with SSH.on(...) run for every host, in parallel with any host handler.
| Handler | Arguments | Runs when |
|---|---|---|
msg |
message |
A message is emitted. |
onData |
data |
Raw data arrives from the stream. |
onStderrData |
data |
Data arrives on stderr. |
onCommandProcessing |
command, response, sshObj, stream |
Output arrives before the prompt is detected. |
onCommandComplete |
command, response, sshObj |
A command returns to the prompt. |
onCommandTimeout |
command, response, stream, connection |
No prompt is detected within idleTimeOut. |
onEnd |
sessionText, sshObj |
The host session finishes. |
onError |
err, type, close, callback |
An error is raised. |
onKeyboardInteractive |
name, instructions, instructionsLang, prompts, finish |
Keyboard-interactive authentication is requested. |
Inside every handler, this is the SSH2Shell instance. Use this.sshObj to
read the current host configuration and this.emit('msg', text) to report
progress.
onCommandProcessing and onCommandTimeout receive the live stream, so you
can answer a prompt with stream.write('y\n'). See
Respond to a prompt mid-command.
If an onError handler needs to close the connection, call
this.connection.end() for the SSH connection or this.close() for the shell
stream. The default error handler closes the shell when its close argument is
true.
Recipes
Run commands on a host
const fs = require('fs');
const SSH2Shell = require('ssh2shell');
const host = {
server: {
host: '192.168.0.10',
port: 22,
userName: 'deploy',
privateKey: fs.readFileSync(process.env.PRIVATE_KEY_PATH),
},
commands: [
'msg:Checking disk usage',
'df -h',
'msg:Checking memory',
'free -m',
],
};
const SSH = new SSH2Shell(host);
SSH.on('msg', (message) => console.log(message));
SSH.connect((sessionText) => {
console.log('--- session ---');
console.log(sessionText);
});
Capture output
The callback receives the combined text from every host. For per-command
output, use commandComplete:
SSH.on('commandComplete', (command, response, sshObj) => {
console.log(`${sshObj.server.host} $ ${command}`);
console.log(response.trim());
});
response is the raw buffer for the command. sshObj.sessionText contains the
accumulated text for the current host.
Authenticate with a private key
const fs = require('fs');
const os = require('os');
const path = require('path');
// Resolve PRIVATE_KEY_PATH (or ~/.ssh/id_rsa) against the home directory.
const keyPath = path.resolve(
(process.env.PRIVATE_KEY_PATH || '~/.ssh/id_rsa')
.replace(/^~(?=$|[\\/])/, os.homedir())
);
const host = {
server: {
host: '192.168.0.10',
userName: 'deploy',
privateKey: fs.readFileSync(keyPath),
passPhrase: process.env.KEY_PASSPHRASE, // omit if the key has no passphrase
},
commands: ['whoami'],
};
~ is not expanded by fs.readFileSync(). Build the path with os.homedir() or
use an absolute path.
If the key has a passphrase and it is wrong, ssh2 reports
Unable to parse private key while generating public key (expected sequence).
Check for typos and for a trailing newline if the value came from a file. Call
.trim() on values read from disk.
See test/live/private_key.js for a runnable
example.
Keyboard-interactive authentication
Set tryKeyboard and provide a handler. The handler must call finish() with
answers in the same order as prompts.
const kbdUser = process.env.KBD_USER || 'kbduser';
const kbdPassword = process.env.KBD_PASSWORD;
const host = {
server: {
host: process.env.KBD_HOST || process.env.HOST,
port: process.env.KBD_PORT || process.env.PORT,
userName: kbdUser,
password: kbdPassword,
tryKeyboard: true,
},
commands: ['whoami'],
};
const SSH = new SSH2Shell(host);
SSH.on('keyboard-interactive', function (name, instructions, instructionsLang, prompts, finish) {
// This example assumes only the password is requested.
finish([this.sshObj.server.password]);
});
SSH.connect();
The live test uses KBD_USER / KBD_PASSWORD because the kbduser
account has password authentication disabled and accepts only the
keyboard-interactive challenge.
Or define onKeyboardInteractive on the host object:
const host = {
server: { /* ... */ tryKeyboard: true },
commands: ['whoami'],
onKeyboardInteractive: function (name, instructions, instructionsLang, prompts, finish) {
finish([this.sshObj.server.password]);
},
};
Keyboard-interactive is only available on the first (primary) connection. A
nested connection made with the ssh command handles authentication through
the shell prompt instead.
See test/live/keyboard-interactive.js
for a complete example.
Verify a host fingerprint
At connection time, ssh2 can hash the server's public key and pass it to
hostVerifier. Compare it with a value you recorded earlier to detect a
different server or a man-in-the-middle.
The verifier receives a hex digest without colons when hostHash is set.
Record the value on the first connection, then pin it:
const fs = require('fs');
const host = {
server: {
host: '192.168.0.10',
userName: 'deploy',
privateKey: fs.readFileSync(process.env.PRIVATE_KEY_PATH),
hostHash: 'sha256', // any crypto.getHashes() algorithm
hostVerifier: function (hashedKey) {
const expectedHash = process.env.EXPECTED_HOST_HASH;
// With no pin recorded, print the hash and accept the first connection.
if (!expectedHash) {
console.log('Server hash:', hashedKey);
return true;
}
return hashedKey.toLowerCase() === expectedHash.toLowerCase();
},
},
commands: ['whoami'],
};
To discover the hash, log hashedKey and accept the first connection:
const host = {
server: {
// ...other settings
hostHash: 'sha256',
hostVerifier: function (hashedKey) {
console.log('Server hash:', hashedKey);
return true;
},
},
commands: ['whoami'],
};
Fingerprint validation applies to the primary connection only. Nested
connections are made by running ssh on the primary host, so their host-key
handling belongs to that host's known_hosts configuration and to the
server.ssh options. See Security.
See test/live/fingerprint.js for a runnable
example.
Run sudo and su commands
sudo is handled automatically. When output matches passwordPrompt,
ssh2shell sends server.password once and continues. A rejected sudo
password is reported as debug output, the command is cancelled, and the session
continues.
If the account has a NOPASSWD sudoers rule, no password is sent. Use sudo -n
to prove that: it fails instead of prompting when a password would be required.
See test/live/sudo_no_pass.js.
const host = {
server: { /* ... */ },
commands: [
'sudo -i',
'cd /root',
'ls -la',
'exit',
],
};
su uses the password of the target user, not the login password, so it is not
automatic. Detect the prompt in onCommandProcessing and answer it with the
stream:
const secondUser = process.env.SECOND_USER || 'suuser';
const host = {
server: {
// ...connection settings
secondUserPassword: process.env.SECOND_USER_PASSWORD, // your own field
},
commands: [
'sudo -i',
'cd /root',
'ls -la',
'exit',
`su ${secondUser}`,
'whoami',
'exit',
],
onCommandProcessing: function (command, response, sshObj, stream) {
if (command === `su ${secondUser}`
&& response.includes('Password:')
&& sshObj.suPasswordSent !== true) {
sshObj.suPasswordSent = true;
stream.write(`${sshObj.server.secondUserPassword}\n`);
}
},
};
The difference between su, sudo su, and sudo -u user -i is explained in
this article.
See test/live/sudo_su.js for a working example.
Respond to a prompt mid-command
Some commands ask a question that is neither a login prompt nor a standard
prompt, such as an installer asking [Y/n]?. Use onCommandProcessing and
write the answer to the stream. Guard it with a flag so it only answers once.
// A local command that prompts, so the example does not need network access.
const promptCommand =
'printf "Install nano [Y/n]? "; read answer; echo "answer=$answer"';
const host = {
server: { /* ... */ },
commands: [promptCommand],
onCommandProcessing: function (command, response, sshObj, stream) {
if (command === promptCommand
&& response.includes('[Y/n]?')
&& sshObj.answeredNano !== true) {
sshObj.answeredNano = true;
stream.write('y\n');
}
},
};
A host handler replaces the default handler for that host. To apply the same logic to every host, attach it to the instance instead:
SSH.on('commandProcessing', function (command, response, sshObj, stream) {
if (command === promptCommand
&& response.includes('[Y/n]?')
&& sshObj.answeredNano !== true) {
sshObj.answeredNano = true;
stream.write('y\n');
}
});
Do not define the same handler in both places. It will run twice.
If no prompt is detected and no answer is sent, commandTimeout fires after
idleTimeOut.
See test/live/prompt_response.js for a
runnable example.
Handle a command timeout
The default timeout handler adds the last response to sessionText and closes
the connection. Override onCommandTimeout if you want to answer the prompt or
keep the session open.
// idleTimeOut defaults to 5000 ms. This command is local so the example does
// not need network access.
const promptCommand =
'printf "Install nodejs [Y/n]? "; read answer; echo "answer=$answer"';
const host = {
server: { /* ... */ },
commands: [promptCommand],
onCommandTimeout: function (command, response, stream, connection) {
if (command === promptCommand
&& response.includes('[Y/n]?')
&& this.sshObj.answeredNode !== true) {
this.sshObj.answeredNode = true;
stream.write('y\n');
return;
}
// Fall back to the default behaviour: record the response and close.
this.emit(
'error',
`${this.sshObj.server.host}: Command timed out after ${this.idleTime / 1000} seconds`,
'Command Timeout',
true,
() => {
this.sshObj.sessionText += response;
}
);
},
};
If you see garbled text before the real response, save the partial response and clear the buffer before writing:
this.sshObj.sessionText += response;
this._buffer = '';
To change timeout handling for every host, leave onCommandTimeout out of the
host object and listen on the instance:
SSH.on('commandTimeout', function (command, response, stream, connection) {
// ...
});
See test/live/command_timeout.js for a
runnable example.
Multiple hosts
Multiple primary hosts
Pass an array of host objects to the constructor. Each host gets its own ssh2 connection, runs its own commands, and is closed before the next host starts. The final callback receives the combined session text.
const fs = require('fs');
// See "Authenticate with a private key" for resolving a ~ path.
const privateKey = fs.readFileSync(process.env.PRIVATE_KEY_PATH);
const host1 = {
server: { host: '10.0.0.11', userName: 'deploy', privateKey },
commands: ['hostname', 'uptime'],
};
const host2 = {
server: { host: '10.0.0.12', userName: 'deploy', privateKey },
commands: ['hostname', 'uptime'],
};
const SSH = new SSH2Shell([host1, host2]);
SSH.connect((allSessionText) => {
console.log(allSessionText);
});
Each host is independent. Shared functions can be assigned to multiple host objects, and shared handlers can be added to the instance instead.
See
test/live/multiple_primary_hosts.js.
Tunnelling through a jump host
Set hosts on a host object to connect to further hosts through it. Each
nested connection is an ssh command run inside the current session, so it uses
the shell of the host you are already on.
const fs = require('fs');
const jump = {
server: {
host: '10.0.0.10',
userName: 'deploy',
privateKey: fs.readFileSync(process.env.PRIVATE_KEY_PATH),
},
commands: ['hostname'],
};
const target = {
server: {
host: '192.168.1.20',
userName: 'deploy',
ssh: {
// Key as it exists on the jump host, used by the nested `ssh -i`.
identityFile: process.env.HOP_KEY_PATH || '/home/deploy/.ssh/id_ed25519',
},
},
commands: ['hostname', 'uptime'],
};
jump.hosts = [target];
const SSH = new SSH2Shell(jump);
SSH.connect((sessionText) => console.log(sessionText));
identityFile must be an absolute path on the jump host; the value is
single-quoted, so ~ is not expanded. For multi-hop chains, use
HOP_KEY_PATH for the first hop and HOP2_KEY_PATH for the second (see
test/live/chaining.js).
Nested hosts can be chained to any depth:
jump.hosts = [bastion];
bastion.hosts = [target];
target.hosts = [];
The process is:
- Connect to the primary host and run its commands.
- For each entry in its
hostsarray, save the current host and load the nested host. - Run
sshon the current host to reach the nested host. - Run the nested host's commands, then process its own
hostsarray. - Restore the previous host and continue with its remaining
hostsentries. - Close all sessions and pass the combined text to the callback.
A nested host object must be defined before it is added to another hosts
array.
server.ssh options. These options apply only to nested ssh connections.
The primary connection uses ssh2 directly.
const host = {
server: {
// ...connection settings
ssh: {
forceProtocolVersion: 2, // 1 or 2; true selects 2
forceAddressType: 4, // 4 or 6; true selects 4
disablePseudoTTY: false, // true adds -T
forcePseudoTTY: false, // true adds -t
verbose: false, // true adds -v
cipherSpec: '', // -c
escape: '', // -e
logFile: '', // -E
configFile: '', // -F
identityFile: '', // -i
loginName: '', // -l
macSpec: '', // -m
options: {}, // -o key=value for each entry
},
},
commands: ['hostname'],
};
| Property | Maps to | Notes |
|---|---|---|
forceProtocolVersion |
-1 / -2 |
1, 2, or true (selects -2). Any other value raises a Security Alert error. |
forceAddressType |
-4 / -6 |
4, 6, or true (selects -4). Any other value raises a Security Alert error. |
disablePseudoTTY |
-T |
Disables pseudo-terminal allocation. |
forcePseudoTTY |
-t |
Forces pseudo-terminal allocation. |
verbose |
-v |
Verbose ssh output. |
cipherSpec |
-c |
Cipher specification. |
escape |
-e |
Escape character. |
logFile |
-E |
Log file path. |
configFile |
-F |
Config file path. |
identityFile |
-i |
Identity file path. |
loginName |
-l |
Login name. |
macSpec |
-m |
MAC specification. |
options |
-o key=value |
Each own enumerable entry. Keys are reduced to [a-zA-Z0-9]; values are shell-quoted. |
The nested command is always built as:
ssh -x <flags> -o "StrictHostKeyChecking=no" -p <port> '<userName>'@'<host>'
userName and host are shell-quoted, and port is reduced to digits.
options keys and values containing shell control characters are rejected with
a Security Alert error.
Note:
StrictHostKeyChecking=nois added to every nested connection. If that is not acceptable, enforce host-key policy in the primary host's~/.ssh/configorknown_hosts, or avoid tunnelling for those hosts.
Connection-level features such as keyboard-interactive apply to the primary
connection only. Each nested host uses its own connectedMessage,
readyMessage, and closedMessage.
See test/live/tunnel.js and
test/live/chaining.js.
Events and the API
Instance events
Add these with SSH.on(name, handler). All handlers run with this set to the
SSH2Shell instance.
SSH.on('msg', (message) => { /* ... */ });
SSH.on('data', (data) => { /* ... */ });
SSH.on('stderrData', (data) => { /* ... */ });
SSH.on('commandProcessing', (command, response, sshObj, stream) => { /* ... */ });
SSH.on('commandComplete', (command, response, sshObj) => { /* ... */ });
SSH.on('commandTimeout', (command, response, stream, connection) => { /* ... */ });
SSH.on('end', (sessionText, sshObj) => { /* ... */ });
SSH.on('error', (err, type, close = false, callback) => { /* ... */ });
SSH.on('keyboard-interactive', (name, instructions, instructionsLang, prompts, finish) => { /* ... */ });
| Event | Arguments | Description |
|---|---|---|
msg |
message |
Status and progress text. Defaults to console.log. |
data |
data |
Raw data from the shell stream. |
stderrData |
data |
Data from stderr. Defaults to console.error. |
commandProcessing |
command, response, sshObj, stream |
Output received before the prompt is detected. |
commandComplete |
command, response, sshObj |
A command returned to the prompt. |
commandTimeout |
command, response, stream, connection |
No prompt was detected within idleTimeOut. |
end |
sessionText, sshObj |
A host session finished. On the final host, sessionText is the combined text. |
error |
err, type, close, callback |
An error occurred. close requests a connection close; callback(err, type) runs before closing. |
keyboard-interactive |
name, instructions, instructionsLang, prompts, finish |
The server requested keyboard-interactive authentication. |
newPrimaryHost is used internally to move between primary hosts and is not
part of the public API.
Methods and properties
SSH2Shell extends the legacy stream.Stream class, so on, once, emit,
removeListener, and pipe are available.
| Member | Description |
|---|---|
connect(callback) |
Start the session. callback(sessionText) is optional and receives the combined text when all hosts finish. |
close() |
Close the current shell stream. A no-op if the shell has not opened yet. |
emit(name, ...args) |
Raise an event. Inherited from EventEmitter. |
sshObj |
The current host object, including runtime state such as sessionText, commands, and pwSent. |
command |
The command currently being processed. |
idleTime |
The active command timeout in milliseconds. |
dataIdleTime |
The active response-settling delay in milliseconds. |
connect() does not return a Promise. Use the callback or the end event to
know when the session is complete.
pipe() is inherited from the stream base class. It is not part of the host
configuration and ssh2shell does not emit pipe or unpipe events.
Advanced usage
Run a generated bash script
For multi-line logic, generate a script on the host, run it, and remove it. Keep
the generated content out of sessionText if you do not want it echoed.
host.commands = [
"cat > myscript.sh <<'EOF'\n" +
'#!/bin/bash\n' +
'current=$(pwd)\n' +
'cd ..\n' +
'if [ -f myfile ]; then\n' +
' sed \'/^[ \\t]*$/d\' "${current}/myfile" | while read -r line; do\n' +
' printf \'Doing some stuff\\n\'\n' +
' printf \'%s\\n\' "$line"\n' +
' done\n' +
'fi\n' +
'EOF',
'chmod 700 myscript.sh',
'./myscript.sh',
'rm myscript.sh',
];
Using a quoted heredoc (<<'EOF') prevents the outer shell from expanding
$current and $line while the script is being written. They are expanded
when the script runs.
If the script contains # or > at the end of a line, it can collide with the
default standardPrompt (">$%#"). Set a narrower prompt for that host, such as
standardPrompt: "$%", or avoid those characters at line ends.
A more robust approach is to base64-encode the script and decode it on the host.
This avoids quoting problems with echo and printf:
echo "<base64>" | base64 -d | bash
echo "<base64>" | base64 -d | bash -s
See test/live/bash_script.js for a runnable
example.
From issue #103 and issue #104.
Verbose and debug output
verbose: trueemits the response text after each command.debug: trueemits internal process steps, including the path through the state machine.
Both are emitted as msg events. Messages are prefixed with the host only when
the session spans more than one host (multiple primary hosts or nested hosts); a
single-host session omits the prefix. Do not enable them inside
onCommandProcessing, which runs for every chunk of output.
Add your own messages with the same flags:
onCommandProcessing: function (command, response, sshObj, stream) {
if (sshObj.verbose) {
this.emit('msg', `${sshObj.server.host}: response: ${response}`);
}
},
Debug and verbose output redacts the configured password and passphrase by
default. Set exposeCredentials: true only while diagnosing an authentication
problem, and never leave it enabled in production or in logs.
Progress indicator
Progress is on by default. It prints a liveness indicator while a command is in
flight, writes to process.stderr, and only runs when stderr is a TTY, so
redirected output, CI logs, and the live-test runner stay clean. It never
changes sessionText or the msg stream.
A dot is written once per second while the command is outstanding. The line is
finished with a newline when the command completes, times out, errors, or the
host or session closes. When the session spans multiple primary or nested
hosts, the first line for each host starts with [host].
Set progress: false on a host to opt out. Progress is also suppressed while
debug or verbose is enabled, because those already report internal steps.
const host = {
// ...
progress: false,
commands: ['apt-get update', 'sleep 30'],
};
Progress output is separate from verbose and debug. When either is enabled,
the heartbeat is suppressed because those modes already report each internal
step. Enable progress only when you want a terminal heartbeat for long or
silent commands; it produces nothing when stderr is redirected to a file.
Credential handling
exposeCredentialsdefaults tofalse. Debug and verbose strings have the configured password and passphrase replaced with[REDACTED], and object dumps maskpasswordandpassphrasekeys.- Credentials are only redacted from
ssh2shell's own debug and verbose output. Your ownmsg,data, andcommandCompletehandlers receive raw text and must redact it if you log it. - Prefer environment variables or a secrets manager over hard-coded passwords in source files.
Troubleshooting
The script hangs after a command. The output did not match
standardPrompt. Check the actual prompt and set standardPrompt to the
characters it ends with. Enable debug: true to see the state transitions.
Unable to parse private key while generating public key (expected sequence).
The passphrase is wrong, or the key is encrypted with an unsupported format.
Recheck the passphrase, try connecting manually with the same value, and
.trim() values read from files.
Password was incorrect for sudo. The sudo password was rejected. The
command is cancelled and the session continues; the message is debug output, not
a terminating error.
A nested host fails to authenticate. Nested connections use the ssh
command on the primary host. Confirm that the primary host can reach the target
and that the supplied userName, password, passPhrase, or identityFile
works from that host.
The output contains escape codes or control characters. Adjust
disableColorFilter, disableASCIIFilter, textColorFilter, or asciiFilter.
See Prompts and response filters.
The connection closes without running commands. Check the error event.
Common causes are a wrong host or port, a rejected key, or an invalid
standardPrompt.
Security
Keep ssh2shell and its dependencies up to date. The library validates and
quotes values that are interpolated into the nested ssh command, but the
application is still responsible for validating untrusted input before it
reaches server or server.ssh.
- Allow-list
host,userName, andportat the application boundary. - Treat
server.sshvalues (identityFile,cipherSpec,escape,logFile,configFile,loginName,macSpec, andoptions) as configuration, not user input. - Nested connections add
StrictHostKeyChecking=no. If that is not acceptable, enforce host-key policy on the primary host or avoid tunnelling. - Keep
exposeCredentialsat its default (false) in production. - Use fingerprint validation on primary connections where host identity matters.
Advisory: GHSA-4j47-m47f-3jp8
covers argument injection and shell breakout through unquoted server.ssh
parameters and the host/userName/port fields. It is fixed in 2.0.13,
2.0.14, and 3.0.0. Users of 2.0.12 and earlier should upgrade.
Development
git clone https://github.com/cmp-202/ssh2shell.git
cd ssh2shell
npm install
npm test
npm test checks that lib/ matches src/, runs the offline unit tests, and
runs the level-aware baseline tests. The live tests need real hosts. See
test/README.md for the test layout, live runner,
environment variables, keys, and VM provisioning.