Summary
In both exec() and shell(), the check that decides whether to send an x11-req compares opts.x11 against the string 'number' rather than using typeof. As a result, passing a numeric screen number — which the TypeScript declarations document as supported — silently does nothing: no x11-req is sent, no error is raised, and X11 forwarding simply never happens.
Version: 1.17.0 (latest published). Also present on master at the time of writing, at the same lines.
The code
lib/client.js, in exec() at 1235-1238 and identically in shell() at 1296-1299:
if ((typeof opts.x11 === 'object' && opts.x11 !== null)
|| opts.x11 === 'number' // <-- compares against the string "number"
|| opts.x11 === true) {
todo.push(() => reqX11(chan, opts.x11, reqCb));
}
opts.x11 === 'number' is true only when the caller literally passes the string 'number'. A real screen number such as 0 or 1 falls through all three branches, so reqX11 is never queued.
Why this looks supported
@types/ssh2 declares it as accepted, in both option types:
export interface ExecOptions {
/** Set either to `true` to use defaults, a number to specify a specific screen number, or an object containing x11 settings. */
x11?: X11Options | number | boolean;
}
export interface ShellOptions {
/** Set either to `true` to use defaults, a number to specify a specific screen number, or an object containing x11 settings. */
x11?: X11Options | number | boolean;
}
So a caller following the types passes a number, gets no error, and no forwarding.
Reproduce
Against any server with X11Forwarding yes:
// Silently no-ops: no x11-req is sent.
conn.shell({ x11: 0 }, (err, stream) => { /* remote DISPLAY is unset */ });
// Works.
conn.shell({ x11: true }, (err, stream) => { /* … */ });
conn.shell({ x11: { screen: 0 } }, (err, stream) => { /* … */ });
The difference is observable on the remote side: echo $DISPLAY is empty in the first case and set in the other two.
Suggested fix
if ((typeof opts.x11 === 'object' && opts.x11 !== null)
|| typeof opts.x11 === 'number'
|| opts.x11 === true) {
reqX11 already ignores a bare number — it only reads single, screen, protocol and cookie from an object — so a caller passing a number would get defaults rather than their requested screen. If the intent is that numbers were never meant to be supported, the fix would instead be to drop number from the two type declarations; either way the code and the types currently disagree.
Related, and possibly documentation rather than a bug
shell() reassigns its arguments at 1259-1262:
if (wndopts && (wndopts.x11 !== undefined || wndopts.env !== undefined)) {
opts = wndopts;
wndopts = undefined;
}
This is presumably how shell(opts, cb) is disambiguated from shell(wndopts, opts, cb). The consequence is that putting x11 (or env) into the window options object silently discards term, rows and cols, and the session gets a default PTY instead.
Measured against a real sshd:
shell({ term, cols: 203, rows: 47 }, cb) -> TERM=xterm-256color 47x203
shell({ term, cols: 203, rows: 47, x11: {...} }, cb) -> TERM=vt100 24x80
shell({ term, cols: 203, rows: 47 }, { x11: {...} }, cb) -> TERM=xterm-256color 47x203
The three-argument form is correct and works. This is easy to get wrong when adding X11 to an existing shell() call that already passes window options, and the silent downgrade of the PTY is hard to attribute. A note in the docs would probably be enough.
Happy to open the type-declarations side separately against DefinitelyTyped if that's preferred.
Summary
In both
exec()andshell(), the check that decides whether to send anx11-reqcomparesopts.x11against the string'number'rather than usingtypeof. As a result, passing a numeric screen number — which the TypeScript declarations document as supported — silently does nothing: nox11-reqis sent, no error is raised, and X11 forwarding simply never happens.Version: 1.17.0 (latest published). Also present on
masterat the time of writing, at the same lines.The code
lib/client.js, inexec()at 1235-1238 and identically inshell()at 1296-1299:opts.x11 === 'number'is true only when the caller literally passes the string'number'. A real screen number such as0or1falls through all three branches, soreqX11is never queued.Why this looks supported
@types/ssh2declares it as accepted, in both option types:So a caller following the types passes a number, gets no error, and no forwarding.
Reproduce
Against any server with
X11Forwarding yes:The difference is observable on the remote side:
echo $DISPLAYis empty in the first case and set in the other two.Suggested fix
reqX11already ignores a bare number — it only readssingle,screen,protocolandcookiefrom an object — so a caller passing a number would get defaults rather than their requested screen. If the intent is that numbers were never meant to be supported, the fix would instead be to dropnumberfrom the two type declarations; either way the code and the types currently disagree.Related, and possibly documentation rather than a bug
shell()reassigns its arguments at 1259-1262:This is presumably how
shell(opts, cb)is disambiguated fromshell(wndopts, opts, cb). The consequence is that puttingx11(orenv) into the window options object silently discardsterm,rowsandcols, and the session gets a default PTY instead.Measured against a real sshd:
The three-argument form is correct and works. This is easy to get wrong when adding X11 to an existing
shell()call that already passes window options, and the silent downgrade of the PTY is hard to attribute. A note in the docs would probably be enough.Happy to open the type-declarations side separately against DefinitelyTyped if that's preferred.