Command not found exit code 127 in GitHub Actions
A command not found exit code 127 failure in GitHub Actions means bash searched every directory on PATH, found nothing with that name, and gave up before your program ran. Install the tool in an earlier step, or add the directory that already holds it to PATH.


What this error means
A run step fails on the first line that calls a tool, printing a path, a line number, the tool name and "command not found". The path before the colon is the temporary script the runner generated for that step, not a file of yours, which is why searching your repository finds nothing. A second wording says the same thing from the shebang line, where /usr/bin/env reports that the interpreter does not exist. Both end the step with exit code 127. The GNU Bash manual is explicit about the number: "if a command is not found, the child process created to execute it returns a status of 127".
/home/runner/work/_temp/lk-repro-step.sh: line 1: pnpm: command not found
##[error]Process completed with exit code 127.
/usr/bin/env: ‘python4’: No such file or directoryReproduced on a Latchkey runner
PATH=/opt/latchkey/bin:/home/linuxbrew/.linuxbrew/bin:/home/linuxbrew/.linuxbrew/sbin:/opt/pipx_bin:/usr/share/rust/.cargo/bin:/usr/local/go/bin:/usr/share/swift/usr/bin:/home/runner/.dotnet/tools:/usr/local/lib/android/sdk/cmdline-tools/latest/bin:/usr/local/lib/android/sdk/platform-tools:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/snap/bin
pnpm on this image: /usr/local/bin/pnpm
/home/runner/work/_temp/lk-repro-step.sh: line 1: pnpm: command not found
##[error]Process completed with exit code 127.
/usr/bin/env: ‘python4’: No such file or directory
env shebang -> exit 127
/home/runner/.latchkey-job-nnJo9Y/_temp/d57e58aa-c003-46c1-a269-45f0cfaed653.sh: line 62: /home/runner/build.sh: Permission denied
not executable -> exit 126
/home/runner/.latchkey-job-nnJo9Y/_temp/d57e58aa-c003-46c1-a269-45f0cfaed653.sh: line 70: mytool: command not found
on disk, not on PATH -> exit 127
mytool 1.0.0
same binary, directory on PATH -> exit 0
[latchkey-bash-wrapper] BEGIN sidecar POST (boot_wait=30s max_time=320s url=http://localhost/diagnose socket=/run/latchkey-self-heal/sock)
[latchkey-bash-wrapper] END sidecar POST ok (attempts=1 http=200)The runner diagnosed the failure and did not retry it; this failure needs the fix below.
127 or 126? The two are one character apart
Read the number before you install anything. Our recorded run put four cases in one step on a latchkey-small runner on 20 September 2026.
A tool that is not on PATH gave 127, and so did a shebang naming an interpreter that does not exist. A script that was present on disk but not marked executable gave 126, with the message "Permission denied" rather than "command not found". The Bash manual draws the same line: "if a command is found but is not executable, the return status is 126".
The fourth case is the useful one. The same mytool binary, executable and on disk throughout, exited 127 when its directory was not on PATH and 0 when it was. Nothing about the file changed between those two lines, which is what makes 127 a search-path failure first and an installation failure second.
Common causes
The tool is not on the image
The image does not ship it and no setup action or install command ran first. This is the majority case, and it is most common with package managers and CLI tools that a developer machine acquires by convention and a runner never does.
It was installed into a directory PATH does not carry
An installer that writes to $HOME/.local/bin, a language package manager with its own bin directory, or an extracted tarball. The binary is present and executable and the shell still cannot find it, which wastes the most time because reinstalling changes nothing.
The PATH change was made in the step that needed it
Writing to GITHUB_PATH affects subsequent steps only, so an install step that appends the directory and then calls the tool fails while the identical call in the next step succeeds. That looks like flakiness and is not.
A typo, or a repository script called without a path
A misspelled binary never matches anything, and neither does a script invoked as build.sh when PATH does not include the working directory. In our experience the second is the one that survives review, because the line looks correct.
How to fix it
Provision the tool in an earlier step and prove it resolves
- Add the vendor setup action, or an explicit install command, as a step before the one that fails.
- Add a one-line version check immediately after it, so a provisioning problem fails on a clear line instead of three steps later.
- Keep the version pinned in the workflow or in
packageManager, so every step agrees on which one is installed.
- uses: pnpm/action-setup@v6
with:
version: 9
- uses: actions/setup-node@v7
with:
node-version: 22
cache: pnpm
- run: pnpm --version
- run: pnpm install --frozen-lockfileAppend the install directory to GITHUB_PATH in the install step
When the tool is installed by a script rather than an action, tell the job where it went. The change reaches every later step in the job, and it is the supported way to extend PATH across steps.
- name: Install mytool
run: |
./scripts/install-mytool.sh --prefix "$HOME/.local"
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
- run: mytool --versionCall repository scripts by path
PATH does not include the working directory, deliberately. A script that lives in the repository is invoked with a path, and it needs the executable bit committed or an explicit interpreter.
- run: ./scripts/build.sh # not: build.sh
- run: bash scripts/build.sh # works without the executable bit
# commit the bit once instead:
# git update-index --chmod=+x scripts/build.shCheck the interpreter when the message comes from env
A shebang naming an interpreter that is not installed reports through /usr/bin/env and still exits 127. Fix the shebang or install the interpreter; do not add the script directory to PATH, because the script was found and its interpreter was not.
head -1 scripts/tool.py # #!/usr/bin/env python3, not python4
command -v python3 || sudo apt-get install -y python3What the reproduction changed on the machine
One thing about the machine, and it is the only difference between our runner and a GitHub-hosted one. The Latchkey image ships pnpm and a GitHub-hosted runner does not, so the first case ran with every PATH directory holding a pnpm executable removed for that single command. Nothing was uninstalled, no other step was affected, and the log records the pnpm path the image does have.
Two details in that output are the script imitating a runner rather than a runner speaking. The lk-repro-step.sh file named in the first line is one our script wrote so bash would report a generated step script as the program name, the shape Actions produces; and the Process completed with exit code 127. line is echoed by the script, not by the Actions wrapper. Everything else is the shell reporting for itself.
The run exited 127, and the failure is deterministic: nothing about a second attempt puts the binary on the machine.
GITHUB_PATH applies to the next step, not this one
The single most common false fix is adding a directory to PATH and calling the tool in the same step. GitHub documents the behavior precisely: writing to GITHUB_PATH "prepends a directory to the system PATH variable and automatically makes it available to all subsequent actions in the current job; the currently running action cannot access the updated path variable".
So an install script that appends its own bin directory to GITHUB_PATH and runs the tool two lines later still reports 127, while the next step works. If you need the tool in the same step, export PATH in the shell as well.
The same distinction catches people with setup-node and a package manager. An action that provisions a tool adds it to PATH for the steps after it, so ordering the provisioning step first is not a style preference, it is the mechanism.
- name: Install and expose
run: |
curl -sSfL https://example.com/install.sh | sh -s -- --bin-dir "$HOME/.local/bin"
echo "$HOME/.local/bin" >> "$GITHUB_PATH" # for later steps
export PATH="$HOME/.local/bin:$PATH" # for the rest of THIS step
mytool --version
- run: mytool --version # works because of GITHUB_PATHVersion notes: corepack, setup actions and images that drift
pnpm and yarn are the usual names in this error because neither is guaranteed on an image. Corepack activates the version pinned in packageManager, but check the Node version first: its README, read on 20 September 2026, says Corepack is distributed with Node.js from 14.19.0 up to but not including 25.0.0, so on Node 25 and later you install it yourself with npm install -g corepack. The vendor setup actions do the same job with an explicit version.
Images drift too, in both directions. A tool that was on ubuntu-22.04 may not be on ubuntu-24.04, and a managed runner image may carry tools GitHub does not. Never rely on a tool you did not install unless the image documentation lists it.
The last variant is not a missing tool at all. A script invoked as build.sh rather than ./build.sh is looked up on PATH, which does not include the working directory, so it reports 127 while sitting in front of you.
- run: npm install -g corepack # Node 25 and later only: no longer bundled
- run: corepack enable
- run: pnpm --version # prove it resolves before you depend on it
- run: pnpm install --frozen-lockfileHow to prevent it
- Provision every tool your workflow uses, and never rely on an image shipping one.
- Print
tool --versionright after provisioning so failures name the right step. - Write PATH changes to
GITHUB_PATH, and remember they apply to later steps. - Invoke repository scripts by path, with the executable bit committed.
Frequently asked questions
What does exit code 127 mean in GitHub Actions?
Why is pnpm not found when setup-node ran first?
setup-node installs Node, not pnpm, and its cache: pnpm option needs pnpm already on PATH to find the store. Run pnpm/action-setup or corepack enable before setup-node, then let the cache option do its work. Corepack is bundled with Node.js from 14.19.0 up to but not including 25.0.0, so on Node 25 and later add npm install -g corepack first.What is the difference between exit code 126 and 127?
chmod or the right binary, 127 is PATH or an install.Why does my PATH change not apply to the step that made it?
GITHUB_PATH reaches all subsequent actions in the job and that the currently running action cannot see it. Export PATH inside the step as well if you need the tool immediately.