# GitHub Actions Process completed with exit code 1, and what stopped

> GitHub Actions Process completed with exit code 1 reports the script status, not the reason. See which commands errexit aborts on and which it skips.

Source: https://latchkey.dev/learn/github-actions/github-actions-run-exit-code-1-set-e  
Updated: 2026-09-20

GitHub Actions Process completed with exit code 1 is the runner reporting the exit status of the script it just ran, with no opinion about which line produced it. Because every run step is invoked with errexit on, that status is usually the status of the first command that returned non-zero, and the number is a better clue than the sentence is.

## What this error means

A run step ends red immediately after a command you did not consider a failure. Common examples are a grep that matched nothing, a test expression that was false, a diff that found a difference, or a linter that found one warning. The log shows the command, shows its ordinary output or no output at all, and then the runner's one-line report. Everything after that command in the same block did not run, and nothing in the log says so.

```Reconstructed from a run step on the default shell; the second line is the runner's own message from ScriptHandler.cs
Run grep "WARN" build.log
Error: Process completed with exit code 1.
```

## Common causes

### A command used a non-zero exit to report an ordinary result

grep returns 1 when nothing matched, diff returns 1 when files differ, and many linters return 1 when they have findings. None of these are errors in the sense you meant, and all of them end the step, because errexit does not know the difference between a report and a failure.

### The failing command was on its own line rather than in a test

The same command is harmless in the test of an `if`, in a `while` condition, after a `!`, or on the left of `&&` or `||`. Standing alone it is an ordinary command in the list and errexit ends the script with its status.

### A multi-line block stops partway and nothing says so

A `run` block is one script. When errexit ends it at line three, lines four onwards never execute and no message marks the boundary, so the log looks as though the last command that printed anything is the last command in the step.

### A helper function ran in an exempt context

The Bash manual is explicit that commands inside a function called from an exempt context are not affected by errexit either. In our experience this produces the opposite complaint, a step that stays green through a failure, and it is worth knowing because it means the exemption list is transitive.

## How to fix it

### Find the command, not the step

1. Scroll to the last command in the block that produced output before the runner's message.
2. Run that command locally and check its exit status with `echo $?`.
3. Decide which of three it is: a real failure, a test you meant to branch on, or a result you will ignore.
4. Apply the matching change rather than suppressing the status of the whole line.

### Branch on the result when it is a test

Move the command into an `if` or a `while` condition, which the Bash manual exempts from errexit explicitly. This keeps the exit status meaningful, because a status you are reading is not a status the shell needs to act on.

```.github/workflows/ci.yml (illustrative)
- run: |
    if grep -q "WARN" build.log; then
      echo "warnings present"
    else
      echo "clean"
    fi
```

### Suppress narrowly, and only where you mean it

Use `|| true` on the single command whose failure you accept, never on a whole compound line, and add a comment saying which non-zero status you are forgiving. A suppression without that note becomes permanent, because the next reader cannot tell whether it was deliberate.

```.github/workflows/ci.yml (illustrative)
- run: grep -c "WARN" build.log || true   # 1 means no matches, which is fine here
```

### Let the step fail visibly when the whole step is optional

If the point is that this work may fail without stopping the job, put `continue-on-error: true` on the step. The step is recorded as failed and the job carries on, which leaves a mark in the run that a swallowed exit code does not. Be aware of what the same key does on a matrix job, which is a separate page in this hub.

## How to prevent it

- Treat exit code 1 as a tool's own report and look up what that tool means by it.
- Keep one logical action per `run` block, so an abort cannot hide later work.
- Write tests as conditions rather than as bare commands followed by a suppression.
- Comment every `|| true` with the status it is forgiving.

## The message is a number, and the number is a fingerprint

The runner does one thing here. It executes the script, asks for the process exit code without requiring it to be zero, and if the code is not zero it writes a single line naming the code and marks the step failed. There is no analysis, no parsing of the output and no attempt to identify the offending line. Whatever number appears in the sentence came out of the process.

Under errexit the shell exits with the status of the command that failed, so the number in the message is the failing tool's own code. That makes 1 informative rather than generic: it is the code that grep returns when it matches nothing, that `test` and `[` return when the expression is false, that `false` returns by definition, and that a large family of linters and formatters return when they found something to report. A missing binary would have given you 127, a signal would have given you something above 128.

So the productive question is not why the step failed but which command exited 1, and the answer is almost always the last command that produced output before the message. That is the line to decide about, and there are only three reasonable decisions: it is a real failure, it is a test you meant to branch on, or it is noise you are prepared to ignore.

## What errexit does and does not abort on

Errexit has a list of exemptions, and knowing it converts most of these failures from a mystery into a one-character edit. The Bash manual states that the shell exits immediately if a pipeline, a list or a compound command returns non-zero, and then names the cases where it does not: a command in the list following a `while` or `until` keyword, a command that is part of the test in an `if` statement, any command in an `&&` or `||` list except the one after the final operator, any command in a pipeline except the last, and a command whose return status is inverted with `!`.

Read that list against your own step and the pattern is clear. A failure used as a decision is exempt, and a failure standing on its own line is not. `grep WARN build.log` on its own aborts. `if grep -q WARN build.log; then ...; fi` does not. `grep -q WARN build.log || true` does not, because the grep is not the command after the final operator.

The manual adds a detail worth keeping. When a compound command or a shell function runs in a context where errexit is being ignored, none of the commands inside it are affected either, even if the option is set. A helper function called from the test of an `if` is therefore unprotected all the way down, which is a real source of surprise in longer scripts.

| How the command appears in the block | Non-zero ends the step | Why |
| --- | --- | --- |
| `grep WARN build.log` | yes | an ordinary command in the list |
| `if grep -q WARN build.log; then` | no | part of the test in an `if` |
| `grep -q WARN build.log || echo none` | no | not the command after the final operator |
| `! grep -q WARN build.log` | no | the status is inverted with `!` |
| `grep WARN build.log | tee out.txt` | no | not the last command in the pipeline |

> The last row is where this meets the pipefail pages in this hub. Without pipefail a pipeline reports only its final command, so errexit never sees the grep's status at all. That is a separate setting with its own page.

## Deciding, rather than suppressing

Appending `|| true` to the failing line makes the step green and is the right answer exactly when you genuinely do not care about the result. It is the wrong answer when you do care, because it also swallows every other reason that command might fail, including the ones you have not met yet: a missing file, a permissions problem, a tool that changed its exit codes in a minor version.

When the result matters, capture it and branch on it. Assigning to a variable inside a command substitution, or following the command with an explicit status check, keeps the distinction between "found nothing" and "could not run" visible. Most tools distinguish those with different codes, and a blanket suppression discards the distinction.

When the whole step is genuinely allowed to fail, say so on the step rather than inside the script. `continue-on-error` on the step records a failure that did not fail the job, which is honest, whereas a suppressed exit code inside a script leaves no trace anywhere.

```.github/workflows/ci.yml (illustrative)
- name: Count warnings
  run: |
    count=$(grep -c "WARN" build.log || true)
    echo "warnings=$count" >> "$GITHUB_OUTPUT"
    if [ "$count" -gt 100 ]; then
      echo "too many warnings" >&2
      exit 1
    fi
```

## Why there is no recorded run on this page

This failure does reach a runner, and it would be easy to record. It would also prove nothing. Any recording would show a script written for the recording failing on a command chosen for the recording, and a reader's step failed on their command, for their reason, with their tool's exit code. The recorded number would be ours, and the number is the only part of the message that carries information.

The claims this page makes are about the shell's option set and about the list of shapes errexit exempts. The first is readable in the runner's argument table and the second is stated in the Bash manual, both quoted above. A log would add a timestamp to facts that are not in dispute and leave the reader's actual question, which command exited 1, exactly where it was.

## FAQ

### Why does my step fail when grep finds nothing?

Because grep returns 1 to report no matches, and a bare command that returns non-zero ends the script under errexit, which every run step is invoked with. Put the grep in the test of an `if`, invert it with `!`, or append `|| true` if the result genuinely does not matter.

### Which commands does set -e not abort on?

The Bash manual lists them: a command in the list following `while` or `until`, a command in the test of an `if`, any command in an `&&` or `||` list except the one after the final operator, any command in a pipeline except the last, and a command whose status is inverted with `!`. Anything else that returns non-zero ends the script.

### Does the exit code in the message tell me which command failed?

Not by name, but usefully. The runner reports the script's exit status, and under errexit that is the status of the command that failed, so 1 points at tools that use 1 for ordinary findings, while 127 points at a missing command and a value above 128 at a signal.

### Should I add set +e to the top of my run block?

Rarely. Turning errexit off for a whole block means the step will pass through every later failure too, including ones you have not anticipated, and the step will report success while doing nothing. Exempt the one command you mean to exempt, or check statuses yourself.

## References

- [GNU Bash manual: the set builtin, the -e option](https://www.gnu.org/software/bash/manual/html_node/The-Set-Builtin.html)
- [actions/runner: ScriptHandler.cs, the exit code branch](https://github.com/actions/runner/blob/main/src/Runner.Worker/Handlers/ScriptHandler.cs)
- [actions/runner: ScriptHandlerHelpers.cs, the default shell arguments](https://github.com/actions/runner/blob/main/src/Runner.Worker/Handlers/ScriptHandlerHelpers.cs)
- [GitHub Actions: workflow syntax, run and shell](https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax)

---

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
