| name | ssh-runner |
| description | Execute read-only commands on remote Linux servers via SSH using the ssh2 npm package. |
| triggers | ["ssh2","ssh runner","remote command execution","ssh npm"] |
When to use this skill
Use this skill when implementing SSH-based remote command execution in Node.js/TypeScript projects using the ssh2 package. Covers connecting, running commands, parsing output, handling auth methods, concurrency, and error handling.
Installation
pnpm add ssh2
pnpm add -D @types/ssh2
Basic Connection and Command Execution
import { Client } from 'ssh2';
import fs from 'fs';
async function runCommand(
host: string,
port: number,
username: string,
keyPath: string,
command: string
): Promise<string> {
return new Promise((resolve, reject) => {
const conn = new Client();
conn.on('ready', () => {
conn.exec(command, (err, stream) => {
if (err) {
conn.end();
reject(err);
return;
}
let stdout = '';
let stderr = '';
stream.on('data', (data: Buffer) => { stdout += data.toString(); });
stream.stderr.on('data', (data: Buffer) => { stderr += data.toString(); });
stream.on('close', (code: number) => {
conn.end();
if (code !== 0 && stderr) {
reject(new Error(stderr.trim()));
} else {
resolve(stdout.trim());
}
});
});
});
conn.on('error', reject);
conn.connect({
host,
port,
username,
privateKey: fs.readFileSync(keyPath),
readyTimeout: 10000,
});
});
}
Running Multiple Commands in One Session
Running each command in a separate session adds overhead. Reuse one connection:
async function runCommands(
config: ConnectConfig,
commands: string[]
): Promise<Map<string, string>> {
return new Promise((resolve, reject) => {
const conn = new Client();
const results = new Map<string, string>();
conn.on('ready', () => {
const runNext = (i: number) => {
if (i >= commands.length) {
conn.end();
resolve(results);
return;
}
conn.exec(commands[i], (err, stream) => {
if (err) { conn.end(); reject(err); return; }
let out = '';
stream.on('data', (d: Buffer) => { out += d.(); });
stream..(, {});
stream.(, {
results.(commands[i], out.());
(i + );
});
});
};
();
});
conn.(, reject);
conn.(config);
});
}
Authentication Methods
SSH Key (recommended)
conn.connect({
host: '192.168.1.10',
port: 22,
username: 'ubuntu',
privateKey: fs.readFileSync('/root/.ssh/id_rsa'),
readyTimeout: 10000,
});
Password
conn.connect({
host: '192.168.1.10',
port: 22,
username: 'ubuntu',
password: 'secret',
readyTimeout: 10000,
});
ConnectConfig type
import type { ConnectConfig } from 'ssh2';
const config: ConnectConfig = {
host: server.host,
port: server.port,
username: server.username,
privateKey: server.keyPath ? fs.readFileSync(server.keyPath) : undefined,
password: server.authMethod === 'password' ? server.password : undefined,
readyTimeout: Number(process.env.SSH_TIMEOUT_MS ?? 10000),
};
Concurrency Limiter
For fleet scans, use a semaphore to cap parallel connections:
async function scanWithConcurrency<T>(
items: T[],
concurrency: number,
fn: (item: T) => Promise<void>
): Promise<void> {
const queue = [...items];
const workers = Array.from({ length: Math.min(concurrency, items.length) }, async () => {
while (queue.length > 0) {
const item = queue.shift()!;
await fn(item).catch((err) => {
console.error('Scan error:', err.message);
});
}
});
await Promise.all(workers);
}
await scanWithConcurrency(servers, SCAN_CONCURRENCY, async (server) => {
await collectServer(server);
});
Parsing Common Linux Command Output
/etc/os-release
function parseOsRelease(raw: string): { name: string; version: string } {
const lines = raw.split('\n');
const kv: Record<string, string> = {};
for (const line of lines) {
const [k, ...rest] = line.split('=');
if (k) kv[k.trim()] = rest.join('=').trim().replace(/^"|"$/g, '');
}
return {
name: kv['PRETTY_NAME'] ?? kv['NAME'] ?? 'Unknown',
version: kv['VERSION_ID'] ?? '',
};
}
free -m
function parseFreeMem(raw: string): { totalMb: number; usedMb: number } {
const line = raw.split('\n').find((l) => l.startsWith('Mem:'));
if (!line) return { totalMb: 0, usedMb: 0 };
const parts = line.split(/\s+/);
return {
totalMb: Number(parts[1]),
usedMb: Number(parts[2]),
};
}
df -m
interface DiskMount {
mountpoint: string;
deviceName: string;
totalMb: number;
usedMb: number;
usedPercent: number;
}
function parseDf(raw: string): DiskMount[] {
const lines = raw.trim().split('\n').slice(1);
return lines
.map((line) => {
const parts = line.trim().split(/\s+/);
if (parts.length < 5) return null;
return {
deviceName: parts[0],
mountpoint: parts[1],
totalMb: Number(parts[2]),
usedMb: Number(parts[3]),
usedPercent: Number(parts[4].(, )),
};
})
.() [];
}
/proc/loadavg
function parseLoadAvg(raw: string): [number, number, number] {
const parts = raw.trim().split(' ');
return [
parseFloat(parts[0]),
parseFloat(parts[1]),
parseFloat(parts[2]),
];
}
/proc/cpuinfo
function parseCpuModel(raw: string): { model: string; cores: number } {
const modelLine = raw.split('\n').find((l) => l.startsWith('model name'));
const model = modelLine?.split(':')[1]?.trim() ?? 'Unknown';
return { model, cores: 0 };
}
Timeout Handling
readyTimeout controls how long to wait for the SSH handshake. If the server is unreachable, ssh2 emits an error event with code: 'ETIMEDOUT' after this duration.
conn.on('error', (err: NodeJS.ErrnoException) => {
if (err.code === 'ETIMEDOUT') {
} else if (err.message?.includes('authentication')) {
} else {
}
reject(err);
});
Error Handling Reference
| Error message | Cause | Resolution |
|---|
connect ETIMEDOUT | Server unreachable or firewall blocking port 22 | Check network, firewall, SSH daemon |
All configured authentication methods failed | Wrong key or key not in authorized_keys | Check key path, permissions, authorized_keys |
ENOENT on key file | Key file not found at keyPath | Check container volume mount and keyPath value |
Handshake failed | SSH server incompatibility or network interruption | Check SSH server version, retry |
read ECONNRESET | Connection dropped mid-session | Retry; may indicate network instability |
Key Security Notes
- Never store private key contents in the database. Store only the file path.
- Never transmit private key contents over the API.
- Mount SSH keys into the container as read-only:
~/.ssh:/root/.ssh:ro.
- Only run read-only commands (
cat, uname, hostname, free, df, nproc). Never sudo, rm, or write commands.
- Use
readyTimeout to prevent hung connections from blocking the scan queue.