# Command not found exit code 127 in GitHub Actions

> A command not found exit code 127 step in GitHub Actions means bash searched PATH and found nothing. Install the tool, or put its directory on PATH.

Source: https://latchkey.dev/learn/failures/command-not-found-exit-127-in-ci  
Updated: 2026-09-20

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".

```Reproduction output, latchkey-small runner, 20 September 2026
/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
```

## 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

1. Add the vendor setup action, or an explicit install command, as a step before the one that fails.
2. Add a one-line version check immediately after it, so a provisioning problem fails on a clear line instead of three steps later.
3. Keep the version pinned in the workflow or in `packageManager`, so every step agrees on which one is installed.

```.github/workflows/ci.yml
- 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-lockfile
```

### Append 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.

```.github/workflows/ci.yml
- name: Install mytool
  run: |
    ./scripts/install-mytool.sh --prefix "$HOME/.local"
    echo "$HOME/.local/bin" >> "$GITHUB_PATH"

- run: mytool --version
```

### Call 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.

```.github/workflows/ci.yml
- 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.sh
```

### Check 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.

```Terminal
head -1 scripts/tool.py           # #!/usr/bin/env python3, not python4
command -v python3 || sudo apt-get install -y python3
```

## How to prevent it

- Provision every tool your workflow uses, and never rely on an image shipping one.
- Print `tool --version` right 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.

## 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.

## What 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.

```.github/workflows/ci.yml
- 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_PATH
```

## Version 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.

```.github/workflows/ci.yml
- 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-lockfile
```

## FAQ

### What does exit code 127 mean in GitHub Actions?

That bash looked for the command on PATH and found nothing. The GNU Bash manual states that a command which is not found returns a status of 127, and the runner reports that as the step result. The tool never started, so nothing inside it failed.

### Why is pnpm not found when setup-node ran first?

Because `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?

126 means the file was found and could not be executed: no executable bit, the wrong architecture, or a broken interpreter line. 127 means nothing with that name was found at all. We measured both in one step, and the fixes are different: 126 is `chmod` or the right binary, 127 is PATH or an install.

### Why does my PATH change not apply to the step that made it?

By design. GitHub documents that a directory written to `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.

## References

- [GNU Bash manual: exit status, 126 and 127](https://www.gnu.org/software/bash/manual/html_node/Exit-Status.html)
- [GitHub Actions: workflow commands and GITHUB_PATH](https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-commands)
- [GitHub-hosted runners: images and installed software](https://docs.github.com/en/actions/reference/runners/github-hosted-runners)
- [pnpm/action-setup: provisioning pnpm on a runner](https://github.com/pnpm/action-setup)

---

Latchkey runs CI/CD that repairs its own failures. Agent entry points: https://latchkey.dev/agent.txt, https://latchkey.dev/openapi.json, https://latchkey.dev/llms.txt
