Watch GitHub PR CI checks in the background and notify on completion. Use when: (1) User wants to monitor CI/CD status, (2) User says 'watch CI', 'check CI', 'monitor checks', or 'wait for CI', (3) User wants to know when checks pass or fail. Runs a background gh polling shell loop (NOT a subagent — near-zero token cost), sends macOS notification on completion. Also handles merged PRs by watching the target branch CI.
Scanned 9/8/2026
Install to Claude Code
npx -y skills add Takazudo/claude-resources --skill watch-ci --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Watch Ci?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/takazudo-watch-ci)More formats (shields.io, HTML) on the badges page.
---
name: watch-ci
description: "Watch GitHub PR CI checks in the background and notify on completion. Use when: (1) User wants to monitor CI/CD status, (2) User says 'watch CI', 'check CI', 'monitor checks', or 'wait for CI', (3) User wants to know when checks pass or fail. Runs a background gh polling shell loop (NOT a subagent — near-zero token cost), sends macOS notification on completion. Also handles merged PRs by watching the target branch CI."
---
# Watch CI
Monitor GitHub PR CI checks in the background, notify on completion via macOS system notification.
Also supports watching CI on the merge target branch when a PR is already merged.
The polling itself is a pure shell loop (`gh` CLI + `jq`) launched via `Bash` with `run_in_background: true`. No subagent is spawned — token cost is paid only at launch and at completion, not on every poll cycle.
> **On Claude Code on the web** (`$CLAUDE_CODE_REMOTE=true`): follow [`web/web-mode.md`](../../web/web-mode.md). `gh` is unavailable, so the `gh`-based poll scripts won't run — poll CI through the GitHub MCP instead (`pull_request_read` with `get_check_runs`, or `actions_*` / `get_job_logs`), or the `Monitor` tool. There is no macOS notifier on web; just report status when checks finish. **When this watch is part of a `-m` merge flow** (`/pr-complete -c -w`, or the Merge Mode of `/x-as-pr` / `/x-wt-teams`), follow **web-mode.md §8: poll in-turn and block** until the checks are terminal — Step 3's "launch a background poll and end the turn" model is **terminal-only**, because web has no background-task wakeup, so a background watch + turn-end would leave the merge unfired. Stay in the turn until terminal, then let the caller merge in the same run.
## Scripts
- `scripts/notify.sh` — macOS notification helper
- `scripts/poll-pr-checks.sh <pr-number> [max-min]` — poll an open PR's checks until terminal, then notify
- `scripts/poll-runs.sh <branch> <commit-sha> [max-min]` — poll workflow runs on a branch+commit until terminal, then notify
All scripts default to a 60-minute cap and a 30-second poll interval.
### False-pass guards — do NOT "simplify" these away
A green report from this skill is often what authorizes a merge, so a false pass is the
expensive failure mode. Both poll scripts carry three guards, added after a real incident
where a PR was reported **"All CI passed" while its actual workflow had not started**:
1. **A skipped check is not a passing check.** The original arithmetic was
`passed = total - pending - failed`, which folded skips into the pass count. A single
third-party `skipping` check therefore read as "1/1 ok". Both scripts now count the
`pass` / `success` state explicitly and report skips as their own number.
2. **Checks that GitHub has not registered yet are invisible.** `gh pr checks` lists only
registered checks, so a queued workflow (or one waiting on a concurrency group) does not
appear at all — leaving one unrelated check to stand in for the whole PR.
`poll-pr-checks.sh` therefore cross-checks `gh run list` for the head SHA and refuses to
conclude while any run for that commit is not `completed`. Related: the server-side
`gh run list --commit <sha>` filter has been observed to **omit queued runs** that
`--branch` returns, so both scripts list by branch and match the SHA in `jq`.
3. **A terminal-looking state must hold for 2 consecutive polls** before it is believed,
since both the check list and the run list lag a push by seconds.
**Exit codes:** `0` passed · `1` failed · `2` timeout · **`3` inconclusive** · `64` bad args.
**Exit 3 (`RESULT: INCONCLUSIVE`)** means checks reached a terminal state but *nothing
produced a result* — only skips, or no real checks. That is legitimate for a fully
path-filtered PR, but it is **not** a pass and must never be reported as one: the caller
decides what it means. Treat it the same as a red result for any merge decision.
**When reporting a pass to the user, sanity-check the job count.** If a repo normally runs
N workflows and the result names fewer, say so rather than reporting green — that mismatch
is exactly how the original bug surfaced.
## Workflow
### Step 1: Identify the PR
Determine which PR to watch:
```bash
# If user provides a PR number or URL, use it directly
# Otherwise, detect from current branch
gh pr view --json number,title,url,headRefName,baseRefName,state,mergeCommit --jq '{number,title,url,headRefName,baseRefName,state,mergeCommit}'
```
If no PR is found for the current branch, inform the user and stop.
**Check the PR state:**
- If `state` is `"OPEN"` → proceed to Step 2 (normal PR watch)
- If `state` is `"MERGED"` → proceed to Step 2b (merged PR: watch target branch CI)
- If `state` is `"CLOSED"` (not merged) → inform the user the PR was closed without merging and stop
### Step 2: Show Initial Status (Open PR)
Show the current state:
```bash
gh pr checks <PR_NUMBER> --json name,state,bucket,workflow
```
Report to the user: PR number/title, total checks, current status breakdown (passed/pending/failed).
If checks have already **failed**, skip to Step 5. If they *look* all-green, go to Step 4 — but
read its two confirmations first: an initial snapshot cannot distinguish "everything passed"
from "the real workflow has not registered yet", and taking the fast path on that snapshot is
the documented false-pass bug. Otherwise proceed to **Step 3**.
### Step 2b: Merged PR — Switch to Target Branch CI
When the PR is already merged:
1. Get the base branch and merge commit SHA from Step 1 output
2. Inform the user: "PR #123 is already merged into `main`. Watching CI on `main` for merge commit `abc1234`..."
3. Show initial status, filtered by the merge commit SHA:
```bash
# Filter on headSha CLIENT-side. Server-side `--commit <sha>` has been observed to OMIT
# queued runs that `--branch` does return (poll-runs.sh guard 2) — using it here would
# make a still-queued workflow invisible and push this step into the missing-run branch
# below for a run that is simply not started yet.
gh run list --branch <base-branch> --limit 40 --json databaseId,name,headSha,status,conclusion \
| jq --arg sha '<merge-commit-sha>' '[.[] | select(.headSha | startswith($sha))]'
```
Runs take a few seconds to register after a merge. If none are found, **retry this
same SHA-filtered query** 2-3 more times with a short delay (~10s) before concluding
anything. Do **not** fall back to an unfiltered `gh run list --branch <base-branch>`
as the result path — a run belonging to an unrelated commit can surface there and get
misreported as this merge's outcome. A broader, unfiltered listing may still be shown
alongside the determination below for context, but never in place of it.
4. If the SHA-filtered query still returns nothing after retrying, do **not** report "no
CI detected" from that alone. Run the **push-trigger-check determination** — see
[`references/push-trigger-check.md`](references/push-trigger-check.md), the single
source of truth for this classification — and report one of its three outcomes:
- `EXPECTED_RUN_MISSING` → report a **failure**. A workflow should have run for this
merge commit and did not; name the likely cause per the reference (a skip marker in
the merge commit message, or a merge performed by Actions using `GITHUB_TOKEN`).
- `BENIGN_NO_TRIGGER` → report "no CI expected" **and name the specific reason** (no
matching `push` trigger, excluded by a `branches`/`paths` filter, a disabled
workflow, or Actions disabled repo-wide). Never report bare "no CI detected" without
the reason.
- `INCONCLUSIVE` → surface it as unresolved — never as a pass, never as benign. State
what could not be determined and why (unparseable workflow YAML, an API error, a
truncated changed-file list).
**One escape hatch back to Step 3:** the reference's Step 5 may find a run for this SHA
that this step's listing missed and whose `status` is not yet `completed` (`queued`,
`in_progress`, `waiting`, `requested`). That is "not yet", not a verdict — do **not**
classify it into any of the three outcomes. Go to **Step 3** and poll it instead.
Otherwise do not proceed to Step 3 in this branch — there is nothing to poll.
5. If runs were found (on the initial query or a retry), proceed to **Step 3** to poll
them to a terminal state.
### Step 3: Launch Background Poll (CLI-only, no subagent)
**Do NOT launch a subagent.** Use the Bash tool with `run_in_background: true` to run the polling shell script. The script polls `gh` directly, exits when checks reach a terminal state, fires a macOS notification, and prints a final `RESULT:` line.
For an open PR:
```
bash $HOME/.claude/skills/watch-ci/scripts/poll-pr-checks.sh <PR_NUMBER>
```
For a merged PR:
```
bash $HOME/.claude/skills/watch-ci/scripts/poll-runs.sh <BASE_BRANCH> <MERGE_SHA>
```
Behaviour:
- Polls every 30 seconds (open PR: `gh pr checks`; merged PR: `gh run list --branch ...` with the SHA matched client-side in `jq` — **not** `--commit`, which can hide queued runs)
- On **success**: `notify.sh success` (Glass sound) + `RESULT: PASSED` to stdout, exit 0
- On **failure**: `notify.sh error` (Basso sound) with failed check names + `RESULT: FAILED (<names>)`, exit 1
- On **timeout** (default 60 min): `notify.sh warning` (Purr sound) + `RESULT: TIMEOUT`, exit 2
- On **inconclusive** (runs completed but none produced a `success`, only skips): `notify.sh warning` + `RESULT: INCONCLUSIVE (...)`, exit 3 — this is `poll-runs.sh`'s own guard against reporting a fluke pass (see the false-pass guards above); it is **not** a pass
After launching, tell the user: "Watching CI in background. You'll be notified when it completes."
When the background task completes you'll be notified automatically. Read its output file to see the `RESULT:` line, report to the user, and — if `FAILED` **or** `INCONCLUSIVE` — treat it as a problem, not a green light: for `FAILED`, proceed to Step 5's investigation steps; for `INCONCLUSIVE`, report it as unresolved and do not authorize a merge on it (same handling as the `INCONCLUSIVE` push-trigger-check outcome in Step 2b).
### Step 4: All Checks Passed (Foreground Fast Path)
**This fast path is where the false-pass bug bites hardest**, because it skips the polling
loop that carries the guards. An initial `gh pr checks` snapshot showing "everything green"
is exactly what an un-started workflow looks like. So before taking it:
1. **Confirm nothing is still queued for the head commit** — a run that is `queued` /
`in_progress` means CI has not finished, whatever the check list says:
```bash
SHA=$(gh pr view <PR_NUMBER> --json headRefOid -q .headRefOid)
BR=$(gh pr view <PR_NUMBER> --json headRefName -q .headRefName)
gh run list --branch "$BR" --limit 40 --json headSha,name,status,conclusion \
| jq --arg sha "$SHA" '[.[] | select(.headSha == $sha)]'
```
2. **Confirm at least one check actually passed** — not merely "none failed". A list of
only `skipping` buckets is `RESULT: INCONCLUSIVE`, not a pass.
If either confirmation fails, **do not use this fast path** — fall through to Step 3 and let
the poll script settle it.
Once both hold:
1. Send notification:
```bash
bash $HOME/.claude/skills/watch-ci/scripts/notify.sh success "All CI checks passed! PR #<number>"
```
2. Report the final status summary, naming the jobs that passed and any that were skipped.
### Step 5: CI Check Failed (Foreground Fast Path)
If checks already failed at Step 2/2b:
1. Send notification:
```bash
bash $HOME/.claude/skills/watch-ci/scripts/notify.sh error "CI check failed: <check-name>. PR #<number>"
```
2. Investigate the failure:
```bash
gh pr checks <PR_NUMBER> --json name,state,bucket,link --jq '[.[] | select(.bucket == "fail" or .bucket == "cancel")]'
gh run list --branch <branch> --status failure --limit 5 --json databaseId,name,conclusion
gh run view <run-id> --log-failed
```
3. Analyze and report. For open PRs: offer to fix. For merged PRs: report only, do NOT auto-fix on the target branch.
## Notes
- Polling is a pure shell loop run via `Bash run_in_background: true` — no subagent, no per-cycle token cost. The main conversation only pays at launch and at completion.
- System notifications use macOS `osascript` via `notify.sh`
- The `gh` CLI must be authenticated and have access to the repository
- 30-second polling interval balances responsiveness with API rate limits
- Default cap 60 minutes; override with the optional `[max-min]` arg to either script
- For merged PRs, watches workflow runs on the target branch filtered by merge commit SHA
- The script's stdout (progress lines + final `RESULT:` line) is captured by the background task — read the output file when you get the completion notification
- Each progress line reports `pass / fail / skip / pending` separately rather than a single
`N/M ok` ratio, so a run carrying skipped checks is legible at a glance instead of being
rounded up into the pass count
- **`RESULT: PASSED` is the only line that authorizes a merge.** `RESULT: INCONCLUSIVE`
(exit 3) means nothing actually ran — treat it as not-green and investigate
- On web, where these `gh` scripts can't run, the MCP polling that replaces them needs the
same three guards — skips aren't passes, queued runs block the verdict, and let the state
settle before believing it
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!