A CI run goes red. You open the Actions tab, click the run, click the failed job, expand the step, and scroll past a few hundred lines of “Installing dependencies…” to find the one line that actually matters: expected 200 but got 500. Five clicks and a scroll to read a single sentence.
The gh CLI reads that sentence for you. gh run view --log-failed prints only the log from steps that failed — nothing green, no scrolling. And once you can see failures from the terminal, the temptation is to gate a deploy script on them. That is where the trap is: gh run watch reports a failed run as a success unless you ask it not to. This hack is both halves — the command that saves you the clicks, and the flag that saves you from shipping a red build.
Every block below is real captured output (gh version 2.96.0). To get an honest failed run to read, I pushed a throwaway repo with a workflow whose middle step exits 1, captured the output, and deleted the repo. This is the CLI-side companion to IT-Journey’s GH-600 Evaluation Signals Table — that note catalogs the signals that tell you pass from fail; this is how you read the pass/fail signal without leaving the shell (and the one place gh reads it wrong).
The run went red — find it without the web UI
gh run list is your Actions tab as a table. The failed run is the one with failure in the second column:
$ gh run list --limit 5
completed failure ci: a workflow with one failing step ci main push 29187887484 7s 2026-07-12T09:41:10Z
Grab that run ID (29187887484). gh run view <id> gives you the job-and-step tree, with an X on exactly what broke:
$ gh run view 29187887484
X main ci · 29187887484
Triggered via push less than a minute ago
JOBS
X test in 2s (ID 86637188627)
✓ Set up job
✓ Run echo "installing deps..." && echo "ok"
X unit tests
- Run echo "this step never runs"
✓ Complete job
ANNOTATIONS
X Process completed with exit code 1.
test: .github#9
To see what failed, try: gh run view 29187887484 --log-failed
View this run on GitHub: https://github.com/…/actions/runs/29187887484
Notice the tree already tells the story: unit tests failed (X), and the step after it shows - — it never ran, because the job stopped. You’ll know you’re reading it right when the X marks the first failing step and everything below it is - (skipped). gh even prints the next command to run for you.
The payoff: –log-failed prints only the red
Take gh’s suggestion. --log-failed dumps the log for the failed steps and nothing else — no “Set up job”, no successful step, no scrolling:
$ gh run view 29187887484 --log-failed
test unit tests Run echo "running unit tests"
test unit tests echo "running unit tests"
test unit tests echo "FAIL: expected 200 but got 500" >&2
test unit tests exit 1
test unit tests shell: /usr/bin/bash -e {0}
test unit tests running unit tests
test unit tests FAIL: expected 200 but got 500
test unit tests ##[error]Process completed with exit code 1.
(Real capture, trimmed: each line is prefixed job⇥step⇥ and carries an ISO timestamp and ANSI color codes I stripped for readability. Pipe it through sed 's/\x1b\[[0-9;]*m//g' if the escape codes clutter your terminal.)
There it is — FAIL: expected 200 but got 500 and ##[error]Process completed with exit code 1 — the two lines you clicked five times for, printed the moment you asked. If a run has several jobs and you want only one, scope it with --job:
$ gh run view --job 86637188627 --log-failed
test unit tests running unit tests
test unit tests FAIL: expected 200 but got 500
test unit tests ##[error]Process completed with exit code 1.
The trap: gh run watch says a failed build succeeded
gh run watch follows a run live and blocks until it finishes — the natural thing to reach for in a script that waits for CI before doing the next step. So you’d expect it to exit non-zero when the run fails, like every other well-behaved command. It does not. Watch the exit code:
$ gh run watch 29187887484; echo "EXIT: $?"
Run ci (29187887484) has already completed with 'failure'
EXIT: 0
The run failed. gh run watch printed the word failure to your screen — and then exited 0. By the only signal a script can read, this red build is green. Add --exit-status and the exit code finally matches reality:
$ gh run watch 29187887484 --exit-status; echo "EXIT: $?"
Run ci (29187887484) has already completed with 'failure'
EXIT: 1
Why this actually bites: the deploy that ships red
This is not a trivia-question footgun. The whole reason to watch in a script is to gate the next step on it. Here is that gate, with and without the flag — same failed run both times:
$ if gh run watch 29187887484 >/dev/null 2>&1; then echo "shipping"; else echo "aborting"; fi
shipping
$ if gh run watch 29187887484 --exit-status >/dev/null 2>&1; then echo "shipping"; else echo "aborting"; fi
aborting
The first line shipped a build that failed its tests, silently, because gh run watch told the if the build passed. The flag is the entire difference between a deploy that respects CI and one that ignores it. If you take one thing from this hack: any script that waits on gh run watch needs --exit-status, the same way any pipeline you trust needs set -o pipefail.
If you don’t need to wait — the run is already done — skip watch entirely and read the conclusion straight out of the API, which never lies about its exit signal:
$ gh run view 29187887484 --json conclusion -q .conclusion
failure
That’s the one to reach for in a script gating on an already-finished run: [ "$(gh run view "$id" --json conclusion -q .conclusion)" = success ].
When this goes wrong
--log-failedprints nothing — the run didn’t fail (checkgh run viewfor anX), or it failed setting up rather than in a step (a badruns-on, a missing secret). Fall back to the fullgh run view <id> --logand read from the top.gh run watchreturns instantly with “has already completed” — that’s expected on a finished run; it only streams live while a run is in progress. The exit-code behavior (0 without--exit-status, non-zero with it) is identical either way, which is exactly why the trap is easy to miss in testing: you test against a completed run, it “works”, and the silent-green only bites in production against a live one.could not find any workflows/ wrong repo —ghuses the repo of your current directory. Add-R owner/nameto point it somewhere else.- The log is a wall of ANSI escape codes — you piped it somewhere non-interactive. Strip them:
gh run view <id> --log-failed | sed 's/\x1b\[[0-9;]*m//g'. gh: command not foundor an auth error — installghand rungh auth login; in Actions,ghneedsGH_TOKENin the env — a present secret isn’t enough.
The reflex when a run goes red is to open the browser and start clicking. You don’t have to. gh run view --log-failed prints the failure straight to your terminal — and when you graduate to scripting around it, remember that gh run watch will call that same failure a success until you make it prove otherwise.