Running WSL shell functions from PowerShell
wsl some-command fails for anything defined in ~/.bashrc, because a non-interactive shell never reads it. wsly runs the command in a genuinely interactive Bash session — and leaves you sitting in it when the command succeeds.
github.com/mihai-valentin/wsly · PowerShell, MIT
the error that starts it
PS> wsl project-jump api
/bin/bash: line 1: project-jump: command not found
The command exists. It works perfectly when you type it inside WSL. It is simply not a program — it is a shell function, defined in ~/.bashrc.
And wsl <command> starts a non-interactive shell. Non-interactive Bash does not read ~/.bashrc. So the function does not exist, and neither does anything else your startup files set up: aliases, nvm, pyenv, direnv hooks, any tool that installs itself by appending a line to your profile.
This is correct Bash behaviour, documented and deliberate. It is just a poor fit for "run the thing I normally run".
the second half of the problem
There is a companion annoyance. For a project-jumping command, succeeding and then exiting is useless — the whole point was to end up there. A command that changes directory and then disposes of the shell it changed has achieved nothing.
So the behaviour wanted is: load the interactive environment, run the command, and if it succeeded, hand me the shell. Which is why this is a launcher and not a one-line alias.
why it isn't a one-liner
The obvious fix is to invoke Bash as interactive and pass it your command as program text. That runs into PowerShell's native-command argument marshalling — PowerShell rewrites arguments on their way to a native executable, and passing a fragment of shell source through that layer means quoting rules from two shells fighting over the same string. It works until an argument contains a space, or a quote, or a dollar sign.
So wsly doesn't pass shell source at all. It ships a small Bash helper, translates that helper's path for the active WSL distro, starts interactive Bash with it, and passes your command as individual arguments. The helper sources ~/.bashrc explicitly, runs the command, and opens a shell on success.
The useful property: no string is ever parsed as code by two shells in sequence. Arguments stay arguments. The fragile part of the problem is designed out rather than escaped around.
scope
It is one PowerShell file plus that helper, with no dependencies beyond WSL itself, and an optional completion bridge so Tab still works from PowerShell. It needs a real distro — internal ones like docker-desktop ship no Bash and are explicitly unsupported, which is the kind of thing worth stating rather than letting someone discover.
It is also, incidentally, the first tool here written start to finish by a coding agent other than Claude. Small, sharply-scoped, one genuinely fiddly constraint in the middle — which turns out to be a good shape for that.