GitHub Actions pwsh command not found on an Ubuntu runner
GitHub Actions pwsh command not found is raised while the runner is looking for the shell binary, which is before your script has been started and before any process could have exited. That ordering is the useful part: there is no exit code attached to this failure, so an answer that tells you to go and read one is describing a different problem.

What this error means
A step that declares the PowerShell Core shell fails immediately, with a short line naming the command. Steps around it that use the default shell run normally, which makes the runner look selectively broken. On a GitHub hosted Ubuntu image this is rare because those images ship the shell. On a self hosted runner, a container image built in house or a runner in a minimal base image, it is the expected result of asking for a program that was never installed.
pwsh: command not foundWhere the message comes from, and what it rules out
Before a run step executes, the runner has to decide which program will execute it. For a step that names one of the built in shells it looks that program up on the path, with a flag that says the lookup is mandatory. The lookup helper writes a trace line explaining that the command was not found and suggesting the path variable, and then, because the lookup was mandatory, throws a file not found exception whose message is the command name followed by the words about the command not being found.
So the message is the runner speaking, not a shell. It is produced during the preparation of the step, which happens before the script file is written and long before anything is executed. Nothing has run, therefore nothing has exited, therefore there is no exit status to interpret.
That rules out a whole family of plausible looking answers. An exit code of one hundred and twenty seven, which a shell returns when a command inside a script cannot be found, belongs to a different failure: a script that started, ran, and reached a line naming a program that is not there. Our page on that exit code covers it. If your log has an exit code, you are on that page, not this one.
It also rules out a message from the operating system loader about a missing interpreter. That shape appears when a script with an interpreter line is executed directly, which is again a case where a process started.
| What you see | What had already happened | Where to look |
|---|---|---|
pwsh: command not found, no exit code | nothing, the shell was not found | the runner image and the path |
| A command not found line plus exit code 127 | the script ran and reached that line | the tool the script calls |
| A message about a bad interpreter | the file was executed directly | the script's first line |
| A shell syntax error | the shell started and parsed | the script contents |
Common causes
The runner image does not include PowerShell Core
The direct cause in almost every report. Self hosted machines, container images and minimal bases install what the build needs, and a shell nobody asked for at image build time is not there at job time.
The step was copied from a Windows job
A workflow that grew a Linux matrix leg keeps the shell declaration that made sense on Windows. The step is often small enough that the shell is not really needed at all.
The job runs in a container that differs from the host
A step running inside a container is looked up differently from one running on the host, and a shell present on the machine is not necessarily present in the image the job is using.
The shell string is custom and names a program that is absent
A shell value with arguments is parsed into a command and a format, and the command is looked up like any other. In our experience this version is harder to spot because the eye reads the whole string as configuration rather than as a program name.
How to fix it
Check for an exit code before anything else
- Look at the failing step for an exit status line.
- If there is one, this page is not your failure; the script started and something inside it was missing.
- If there is none and the line is just the command name, the runner could not find the shell.
Use the default shell when the script is small
Drop the shell declaration and translate the commands. On Linux the runner resolves the default by looking for one shell and falling back to another, so you do not need to name anything.
- name: Print the version
run: |
node -p "require('./package.json').version"Install the shell in the image, not in the job
Add it where the runner image is built so every job starts with it present. Installing it as a step costs time on every run and turns a clear failure into a network dependent one.
Probe for the program and fail with a useful message
Where you cannot control every runner, check for the program in an early step and say what is missing. A named assumption is easier to act on than a lookup failure in the middle of a matrix.
- name: Preflight
run: |
command -v pwsh >/dev/null || {
echo "this job needs PowerShell Core on the runner"; exit 1; }Which runners have it, and which branch of the resolver you are in
GitHub hosted Ubuntu images ship PowerShell Core, so a step declaring that shell works there without anything being installed. The failure belongs to runners you or your vendor assembled: a self hosted machine, a container image, or anything built from a minimal base that installs only what the build needs.
The resolver has two relevant branches and they end the same way. For a step in a workflow that names one of the recognized system shells, the runner looks the shell up directly. For a step inside an action, the same name is treated as a shell option string, parsed into a command and arguments, and the command is looked up the same way. Both paths reach the same lookup, and for a step running directly on the runner host that lookup is mandatory, so either one throws with the same message if the program is absent. When the step host is a container the lookup is not required, and the absence shows up inside the container instead.
There is a third branch worth knowing about because it produces a different message. If the shell value is not a recognized built in and does not contain the placeholder that tells the runner where to put the script path, the runner refuses it with a message about the shell option being invalid and lists the built ins it accepts. That is a configuration mistake in the shell string rather than a missing program.
# a step in a workflow: a recognized system shell, looked up directly
- shell: pwsh
run: $PSVersionTable.PSVersion
# a custom shell string: parsed, then the command is looked up
- shell: 'pwsh -NoProfile -Command ". {0}"'
run: $PSVersionTable.PSVersionDeciding between installing it and not needing it
If the step is a fragment of PowerShell that was written for a Windows job and is now running on Linux, the cheapest fix is usually to stop asking for the shell. Translate the handful of commands and use the default, which on Linux the runner resolves by looking for one shell and falling back to another. A step of three lines rarely justifies a dependency.
If the step is real PowerShell, install it in the image rather than in the workflow. Installing a shell as a step means every run pays for it, and it means the failure moves from a clear missing program message to a slower and more confusing network dependent one. Putting it in the image keeps the workflow honest about what it assumes.
Either way, make the assumption visible. A step that will not run without a particular program on the machine should say so somewhere a reader of the workflow will see it, because the next person to add a runner will not know.
- name: Confirm the shell exists before relying on it
run: command -v pwsh || echo "pwsh is not installed on this runner"Why there is no recorded run on this page
Recording this failure would mean building a runner image with the shell deliberately left out and capturing the result, which proves that we can remove a program from a machine. The interesting question is whether it is on yours, and no recording of ours answers it.
The message is instead taken from the lookup helper in the runner, where the thrown exception and its message sit next to the mandatory flag that decides whether it is thrown at all. Reading it there also establishes the ordering, which is the part of this page that actually changes what a reader does next, because it is what tells them to stop looking for an exit code.
There is nothing to repair. A shell that is not installed cannot be conjured mid job, and a runner that quietly substituted a different shell would run your PowerShell through something that does not understand it.
How to prevent it
- Keep the shell values in a workflow to programs the runner image is known to ship.
- Install shells at image build time, never as a workflow step.
- Probe for unusual programs in one preflight step rather than discovering them mid matrix.
- When adding a Linux leg to a Windows matrix, review every shell declaration.
Frequently asked questions
Why is there no exit code with pwsh command not found?
Is this the same as exit code 127?
Do GitHub hosted Ubuntu runners have pwsh?
What happens if I write a custom shell string?
Related guides
References
- actions/runner: WhichUtil.cs, the mandatory lookup and the exception it throws
- actions/runner: ScriptHandler.cs, shell resolution before the script is written
- GitHub Actions: workflow syntax, jobs.<job_id>.steps[*].shell
- actions/runner-images: the software installed on the hosted Ubuntu images
- GitHub Actions documentation