Title: Windows + Claude Desktop: every SSH command fails with exit 255 (empty stderr) — spawned ssh inherits a stripped env missing %ProgramData%
Summary
When the extension runs as a DXT under Claude Desktop on Windows, every operation against a reachable host fails with code: 255 and empty stdout/stderr, while the same ssh command run by hand from PowerShell/cmd succeeds instantly. checkConnectivity returns connected: false, runRemoteCommand/runCommandBatch return { stdout: "", stderr: "", code: 255 }.
Root cause: the ssh.exe child process is spawned inheriting the extension's process.env, and Claude Desktop launches the extension with a stripped, allow-listed environment that omits ProgramData/ALLUSERSPROFILE. Win32‑OpenSSH needs %ProgramData% to locate its global config (%ProgramData%\ssh\) at startup and exits 255 immediately, before producing any output, when it's unset.
Environment
- OS: Windows 11 (x64)
- Claude Desktop: MSIX/Store package (
C:\Program Files\WindowsApps\Claude_*)
- SSH client: bundled Microsoft Win32‑OpenSSH (
C:\Windows\System32\OpenSSH\ssh.exe)
- mcp-ssh: 1.3.8 (also reproduces on the version shipped in the current
.dxt)
- Host: key-based auth (ed25519, no passphrase),
~/.ssh/config alias, StrictHostKeyChecking accept-new
Symptom
// checkConnectivity
{ "connected": false, "message": "Connection failed" }
// runRemoteCommand
{ "stdout": "", "stderr": "", "code": 255 }
Manual run of the exact same command from a normal shell → exit 0.
Root cause
Claude Desktop hands the extension's Node process a curated environment of ~13 variables:
APPDATA, HOMEDRIVE, HOMEPATH, LOCALAPPDATA, MCP_SILENT, PATH,
PROCESSOR_ARCHITECTURE, PROGRAMFILES, SYSTEMDRIVE, SYSTEMROOT,
TEMP, USERNAME, USERPROFILE
Notably missing: ProgramData, ALLUSERSPROFILE, windir, ComSpec, TMP, PATHEXT, …
Because the child ssh inherits this env, and Win32‑OpenSSH resolves %ProgramData% very early to find its global config directory, ssh.exe aborts with exit code 255 before it writes anything (even ssh -E <logfile> -vvv produces no log file).
Evidence (env bisection)
I captured the live extension process's exact environment and replayed the same ssh invocation, adding back one missing variable at a time:
[stripped baseline] EXIT=255 stdout='' ← matches the extension
[+ ComSpec] EXIT=255 stdout=''
[+ windir] EXIT=255 stdout=''
[+ PATHEXT] EXIT=255 stdout=''
[+ ProgramData/ALLUSERS] EXIT=0 stdout='connected' ← fixed
[+ TMP] EXIT=255 stdout=''
Restoring only ProgramData (+ALLUSERSPROFILE) flips ssh from 255 → 0. Confirmed not related to keys, passphrase, askpass, host‑key, the resolved binary, console vs. no‑console, or MSIX network capabilities — all of those reproduce as success.
Affected code (server.mjs)
For key-auth hosts there is no password, so buildSpawnEnv() returns null and spawnOptions.env is left unset → the child inherits the stripped process.env:
async buildSpawnEnv(hostAlias) {
const password = await this.getPasswordForHost(hostAlias);
if (!password) return null; // ← key-only hosts: no env override
return {
...process.env, // ← even here, %ProgramData% is missing
MCP_SSH_PASS: password,
SSH_ASKPASS: askpassScript,
SSH_ASKPASS_REQUIRE: 'force'
};
}
// runRemoteCommand:
if (passwordEnv) { spawnOptions.env = passwordEnv; /* ... */ }
const child = this._spawn(SSH_BIN,
['-o', 'StrictHostKeyChecking=accept-new', '--', hostAlias, command],
spawnOptions);
// uploadFile / downloadFile:
if (passwordEnv) options.env = passwordEnv;
Both paths are affected: password hosts spread a process.env that is itself missing ProgramData, and key-only hosts inherit it implicitly.
Suggested fix
Guarantee the variables Win32‑OpenSSH needs are present for every spawned ssh/scp. Simplest, covers all spawn sites — normalize once at module load:
if (process.platform === 'win32') {
if (!process.env.ProgramData) {
process.env.ProgramData = process.env.ALLUSERSPROFILE || 'C:\\ProgramData';
}
if (!process.env.ALLUSERSPROFILE) {
process.env.ALLUSERSPROFILE = process.env.ProgramData;
}
}
(Or build an explicit base env for every spawn so the child never depends on a host launcher passing a complete environment.) Other consumers under restricted launchers may also benefit from restoring windir/ComSpec/SystemRoot.
Workaround (until released)
Add the same block to the top of the installed server.mjs, or add to the DXT manifest.json → server.mcp_config.env:
"env": {
"MCP_SILENT": "false",
"ProgramData": "C:\\ProgramData",
"ALLUSERSPROFILE": "C:\\ProgramData"
}
then fully restart Claude Desktop. (Manifest env values do reach the process — that's how MCP_SILENT arrives.) Note: setting Windows user/system environment variables does not help, because the host passes only an allow-listed subset.
Title: Windows + Claude Desktop: every SSH command fails with exit 255 (empty stderr) — spawned
sshinherits a stripped env missing%ProgramData%Summary
When the extension runs as a DXT under Claude Desktop on Windows, every operation against a reachable host fails with
code: 255and emptystdout/stderr, while the samesshcommand run by hand from PowerShell/cmd succeeds instantly.checkConnectivityreturnsconnected: false,runRemoteCommand/runCommandBatchreturn{ stdout: "", stderr: "", code: 255 }.Root cause: the
ssh.exechild process is spawned inheriting the extension'sprocess.env, and Claude Desktop launches the extension with a stripped, allow-listed environment that omitsProgramData/ALLUSERSPROFILE. Win32‑OpenSSH needs%ProgramData%to locate its global config (%ProgramData%\ssh\) at startup and exits 255 immediately, before producing any output, when it's unset.Environment
C:\Program Files\WindowsApps\Claude_*)C:\Windows\System32\OpenSSH\ssh.exe).dxt)~/.ssh/configalias,StrictHostKeyChecking accept-newSymptom
Manual run of the exact same command from a normal shell → exit 0.
Root cause
Claude Desktop hands the extension's Node process a curated environment of ~13 variables:
Notably missing:
ProgramData,ALLUSERSPROFILE,windir,ComSpec,TMP,PATHEXT, …Because the child
sshinherits this env, and Win32‑OpenSSH resolves%ProgramData%very early to find its global config directory,ssh.exeaborts with exit code 255 before it writes anything (evenssh -E <logfile> -vvvproduces no log file).Evidence (env bisection)
I captured the live extension process's exact environment and replayed the same
sshinvocation, adding back one missing variable at a time:Restoring only
ProgramData(+ALLUSERSPROFILE) flipssshfrom 255 → 0. Confirmed not related to keys, passphrase, askpass, host‑key, the resolved binary, console vs. no‑console, or MSIX network capabilities — all of those reproduce as success.Affected code (
server.mjs)For key-auth hosts there is no password, so
buildSpawnEnv()returnsnullandspawnOptions.envis left unset → the child inherits the strippedprocess.env:Both paths are affected: password hosts spread a
process.envthat is itself missingProgramData, and key-only hosts inherit it implicitly.Suggested fix
Guarantee the variables Win32‑OpenSSH needs are present for every spawned
ssh/scp. Simplest, covers all spawn sites — normalize once at module load:(Or build an explicit base env for every spawn so the child never depends on a host launcher passing a complete environment.) Other consumers under restricted launchers may also benefit from restoring
windir/ComSpec/SystemRoot.Workaround (until released)
Add the same block to the top of the installed
server.mjs, or add to the DXTmanifest.json→server.mcp_config.env:then fully restart Claude Desktop. (Manifest
envvalues do reach the process — that's howMCP_SILENTarrives.) Note: setting Windows user/system environment variables does not help, because the host passes only an allow-listed subset.