Skip to content

Spawn a WSL subshell when the windows install is called from WSL - #33

Merged
Korijn merged 2 commits into
masterfrom
claude/keycmd-issue-29-ikmw3u
Aug 4, 2026
Merged

Spawn a WSL subshell when the windows install is called from WSL#33
Korijn merged 2 commits into
masterfrom
claude/keycmd-issue-29-ikmw3u

Conversation

@Korijn

@Korijn Korijn commented Aug 4, 2026

Copy link
Copy Markdown
Owner

Fixes #29.

The problem

With the WSL setup, keycmd is a windows process invoked from a shell inside a distribution. shellingham finds no shell in its windows ancestor chain, so get_shell falls back to %COMSPEC%: the command ran in cmd, on the wrong side of the interop boundary, in a shell nobody asked for. The fallback also reported the shell as cmd.exe, which run_cmd did not match against its "cmd" check, so it built cmd.exe -c "<command>" — an option cmd does not understand.

The change

keycmd/wsl.py (new) decides whether keycmd was called from a distribution, and builds the command line for it:

  • a wsl.exe/wslhost.exe/wslservice.exe ancestor decides for it; a windows shell found first decides against it (that is a windows shell run over interop, and its command belongs on windows); a working directory under \\wsl$\ or \\wsl.localhost\ settles the rest
  • that UNC working directory also names the distribution, which is passed on as --distribution so a second distribution does not send the command to the default one
  • KEYCMD_WSL=0/1 overrides the decision in either direction, and --verbose reports the process tree it walked and which way it went
  • ancestors come from the process table snapshot shellingham already reads; when it cannot be read that is logged and detection falls through to the working directory, it never raises

keycmd/shell.py: run_shell opens a shell in the distribution, run_cmd runs the command there (wsl.exe -- <cmd>, which hands it to the distribution's login shell, the counterpart of the -c used elsewhere). The COMSPEC fallback now reports cmd rather than cmd.exe, the way shellingham does, which fixes the /C path for plain windows use too.

keycmd/creds.py: credentials do not cross the boundary by themselves, since neither side inherits the other's environment. Only what is named in WSLENV makes the trip, so get_env adds the keys and aliases it exposes to it, keeping whatever the user already shares, flags and all.

Tests

tests/test_wsl_interop.py fakes the windows process table and working directory, so detection, the wsl.exe command lines and WSLENV are covered on every platform. An autouse outside_wsl fixture keeps the rest of the suite off that code path, so a run on windows still looks like a run anywhere else.

The opt-in end to end test in tests/test_wsl.py now runs keycmd --verbose printenv <VAR> from a WSL shell: printenv is a linux command, so reaching it at all means the command went back into the distribution, and it only sees the credential because of WSLENV.

ruff, ty and the suite pass locally (79 passed, 3 skipped — the skips are the opt-in end to end WSL tests).

Worth a look before merging

The end to end tests are the only check on the real ancestor image names, and they need a windows machine with WSL:

$env:KEYCMD_TEST_WSL = 1
uv run pytest tests/test_wsl.py

If the process table turns out to say something else on your machine, keycmd --verbose prints the chain it walked, and the working directory signal still covers the common WSL2 case of working on the distribution's own file system.


Generated by Claude Code

claude added 2 commits August 4, 2026 20:10
WSL users install keycmd on windows, so that keyring reaches the windows
credential manager instead of a keyring daemon inside the distro. That
leaves keycmd a windows process with a windows idea of a shell: it would
hand the command to cmd, or open one for --shell, neither of which is
what someone typing in a distro shell asked for.

keycmd now works out whether it was called from a distro -- a wsl.exe or
wslhost.exe ancestor decides it, a windows shell found first decides
against it, and a working directory under \\wsl$ settles the rest -- and
hands the command to wsl.exe when it was. KEYCMD_WSL overrides that
decision in either direction, and --verbose reports which way it went.

The credentials do not cross the boundary by themselves, since neither
side inherits the other's environment, so get_env lists the variables it
exposes in WSLENV, the only thing that does make the trip.

Along the way, the COMSPEC fallback in get_shell now reports "cmd"
rather than "cmd.exe", the way shellingham does, so that run_cmd
recognizes it and passes /C instead of an option cmd does not know.

Fixes #29
wsl.exe runs the default distribution unless it is told which one to
run, which is the wrong one as soon as a machine has more than one
installed. A working directory on a distribution's own file system
reaches windows as a UNC path that names it, so pass it on as
--distribution when it does.
@Korijn
Korijn merged commit bcfc8db into master Aug 4, 2026
9 checks passed
@Korijn
Korijn deleted the claude/keycmd-issue-29-ikmw3u branch August 4, 2026 20:25
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.

WSL: spawn WSL subshell when using keycmd.exe (Windows process) from WSL

2 participants