Skip to content

Windows: set %ProgramData%/%ALLUSERSPROFILE% so spawned ssh/scp work under Claude Desktop (fixes #10) - #11

Merged
ferraro merged 3 commits into
AiondaDotCom:mainfrom
pa-bmundt:fix/windows-programdata-env
Aug 10, 2026
Merged

Windows: set %ProgramData%/%ALLUSERSPROFILE% so spawned ssh/scp work under Claude Desktop (fixes #10)#11
ferraro merged 3 commits into
AiondaDotCom:mainfrom
pa-bmundt:fix/windows-programdata-env

Conversation

@pa-bmundt

Copy link
Copy Markdown
Contributor

Fixes #10.

On Windows under Claude Desktop the extension launches with a stripped,
allow-listed environment that omits %ProgramData%/%ALLUSERSPROFILE%. Win32-OpenSSH
resolves %ProgramData% at startup to find its global config (%ProgramData%\ssh)
and exits 255 with no output when it is unset, so every spawned ssh/scp fails even
though the same command works from an interactive shell.

This normalizes the two vars once at module load, Windows only. No behavior change
on macOS/Linux, no transport change, and full ~/.ssh/config handling is preserved
since we still invoke the real ssh. Verified with the bisection from #10 on a
separate box (Windows 11, OpenSSH_for_Windows_9.5p2): empty ProgramData gives
ssh -V exit 255 with no output, setting ProgramData only gives exit 0.

pa-bmundt and others added 3 commits June 9, 2026 19:35
…m %SystemDrive%

Adds real tests for the issue AiondaDotCom#10 fix. They were needed on two counts:

The normalization mutates process.env at import time, and loadServerAs()
only restored variables a test had explicitly overridden. So the first
Windows-flavoured import leaked ProgramData into every later test, and the
second import then took the already-set branch — both branches ended up
'covered' with nothing asserting them. The helper now always saves and
restores the variables the module writes at import, and returns a snapshot
of them (envAfterImport) because the restore happens before a test can
look. Verified by skipping the new block: branch coverage drops to 98.89%
with lines 37 and 44 uncovered, so the coverage is now earned rather than
incidental.

Also derives the last-resort default from %SystemDrive% rather than
hardcoding C:. SystemDrive is in the environment Claude Desktop passes
through, and a Windows install on another drive would otherwise get a path
that does not exist. A trailing separator is tolerated.

Nine tests cover: both variables missing, the SystemDrive-derived default,
a trailing separator, SystemDrive itself missing, an existing
ALLUSERSPROFILE winning over the default, backfilling ALLUSERSPROFILE from
ProgramData, leaving both alone when set, staying inert on POSIX, and the
key-auth spawn path that inherits the repaired environment.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@ferraro
ferraro merged commit f9281f3 into AiondaDotCom:main Aug 10, 2026
6 checks passed
@ferraro

ferraro commented Aug 10, 2026

Copy link
Copy Markdown
Member

Merged — thanks, and thanks for reproducing the bisection independently on your own setup before opening this. Using ssh -V as the probe was the detail that made it airtight: it opens no socket, so it rules out keys, host keys, network and askpass in one shot. Between @Krolikfarm's original bisection in #10 and your confirmation, the diagnosis was never in doubt.

I verified the failing paths still match main before merging. Both are affected exactly as the issue describes: key-auth hosts get no env override, so spawnOptions.env stays unset and the child inherits the stripped process.env, and password hosts spread a process.env that is missing the variable in the first place. Normalizing once at module load covers every spawn site, which is the right shape for this.

Two changes on top before merging.

Derive the default from %SystemDrive%. 'C:\\ProgramData' was hardcoded, so a Windows install on another drive would have got a path that doesn't exist. SystemDrive is in the allow-listed set Claude Desktop passes through (it's in your own list in #10), so it's available exactly when we need it:

const systemDrive = (process.env.SystemDrive || 'C:').replace(/[\\/]+$/, '');
process.env.ProgramData = process.env.ALLUSERSPROFILE || `${systemDrive}\\ProgramData`;

C: remains the last-resort fallback, and a trailing separator is tolerated.

Tests. main now enforces 100% coverage of server.mjs as a build gate (added while merging #13), so the normalization needed real tests. That turned up something worth flagging, because it would have bitten silently: the block mutates process.env at import time, and the test helper that re-imports the module with a faked platform only restored variables a test had explicitly overridden. So the first Windows-flavoured import leaked ProgramData into the process, the next one necessarily took the already-set branch, and both branches reported as covered with nothing asserting them. The helper now always saves and restores what the module writes at import, and returns a snapshot (envAfterImport) since the restore happens before a test can read it.

Verified rather than assumed: skipping the new test block drops branch coverage to 98.89% with lines 37 and 44 uncovered. The nine new tests cover both variables missing, the %SystemDrive%-derived default, a trailing separator, SystemDrive itself missing, an existing ALLUSERSPROFILE winning over the default, backfilling ALLUSERSPROFILE from ProgramData, leaving both alone when already set, staying inert on POSIX, and the key-auth spawn path that inherits the repaired environment — the actual subject of #10.

CI is green across ubuntu-latest and windows-latest on Node 20/22/24 (Windows coverage was added while merging #13), 148 tests, 100% coverage enforced.

One note for anyone hitting this before the next release: the manifest env workaround in #10 still applies, and setting Windows user/system environment variables still won't help, since the host only forwards its allow-list.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Windows/Claude Desktop: all SSH commands fail with exit 255 (empty stderr) — spawned ssh inherits a stripped env missing %ProgramData%

2 participants