Keeps your open PRs up to date and tells you when one runs into a conflict.
A GitHub CLI extension that runs on its own a few times a day.
English · Español
It does what GitHub's "Update branch" button does, but for you and on all your PRs at once. Nothing changes in the repo: no Actions, no bots, no new permissions. It never resolves conflicts, rebases or forces anything: when a PR conflicts, it only notifies you. It also tells you, once, when a review requests changes and, if you turn it on, when CI fails on a branch it updated. If you ask for it, it also approves the PRs of the people you choose once their CI passes.
Built for repos with the "Require branches to be up to date before merging" rule: every merge to the base leaves all open PRs behind, and each one has to be updated before it can merge.
What a run looks like (the same lines go to gh pr-watcher log):
$ gh pr-watcher run
2026-10-06 10:00:04 acme/api#412: 3 behind → branch updated from main (CI will run)
2026-10-06 10:00:06 acme/api#418: up to date
2026-10-06 10:00:09 acme/web#97: CONFLICT (5 behind)
gh extension install ezeed/gh-pr-watcher
gh pr-watcher install # asks for the config and schedules the run
gh pr-watcher run --dry-run # test: reports what it would do, without updating any branchTo pin a version: gh extension install ezeed/gh-pr-watcher --pin v0.3.0 (see releases).
Ask your coding agent (Claude Code, Codex, Cursor…):
Install gh-pr-watcher following https://github.com/ezeed/gh-pr-watcher/blob/main/AGENTS.md
It reads your accounts, orgs and repos with detect and installs with flags, no console prompts:
gh pr-watcher detect
gh pr-watcher install --owners "AcmeCorp" --every 1h- macOS for scheduling (launchd). On Linux,
runworks the same andinstallgives you thecrontabline. ghauthenticated (gh auth login), with write access to each PR's branch. Your PRs in the org's repo already have it. From a fork, the PR needs "Allow edits from maintainers".jq(brew install jq).- Optional:
terminal-notifier(brew install terminal-notifier), so clicking the notification opens the PR. Without it, notifications go throughosascriptand aren't clickable.
It lives in ~/.config/gh-pr-watcher/config: KEY=VALUE lines that are read, never executed.
Changes apply from the next run; only EVERY_MINUTES needs gh pr-watcher install --yes to
reschedule.
| Key | Default | What it controls |
|---|---|---|
GH_USER |
(empty) | gh account to use, even if another one is active. Empty = the active gh account on each run. |
OWNERS |
(empty) | Orgs or users whose repos are watched, space-separated. Empty = all your open PRs, in any repo. The installer suggests your orgs. |
REPOS |
(empty) | Specific owner/repo repos, space-separated. If set, only those are watched and OWNERS is ignored (GitHub search combines org and repo with AND, not OR). |
EVERY_MINUTES |
60 |
How often it runs, in minutes: 15, 30, 60, 120 or 180, counting from 0:00 (180 = 0:00, 3:00, 6:00…). If the machine is asleep at that time, launchd runs it on wake. Each update triggers CI, so a shorter interval can mean more CI runs when several merges land close together. A config with the old EVERY_HOURS keeps working until the next install. |
INCLUDE_DRAFTS |
false |
true = drafts get updated too. |
SKIP_LABEL |
(empty) | PRs with this label are left alone. The installer offers no-autoupdate; in the file you can use any label. |
NOTIFY_IMAGE |
(the logo) | Local image (PNG or JPG) shown on the right of the notification. Only with terminal-notifier. The icon on the left can't be changed: macOS takes it from the notifying app. |
NOTIFY_CHANGES_REQUESTED |
true |
true = one notification when a review moves a PR to changes requested. It isn't repeated while the PR stays that way; if it's approved and changes get requested again, you're notified again. |
NOTIFY_CI_FAILED |
false |
true = one notification when CI fails on a branch the watcher updated. Only on its own merge commit: if you push in between or on top, it stops following it. It stops after 24 h without a result too. |
AUTO_APPROVE_AUTHORS |
(empty) | GitHub logins, space-separated, whose open PRs in the watched orgs or repos get approved as your account. Empty = nothing is approved. |
AUTO_APPROVE_UNTIL |
(empty) | Last day it approves, YYYY-MM-DD, inclusive. Empty = no end date. |
AUTO_APPROVE_REQUIRE_CI |
true |
true = approve only when the PR has checks and every one passed (success, neutral or skipped). Pending, failing or none: it tries again next run. |
A misspelled key (OWNER=) or a line that isn't KEY=VALUE isn't silently ignored: you get a
warning, with a suggestion if it looks like a real key. install also checks that every owner and
repo exists and that your account can see it, and that every login in AUTO_APPROVE_AUTHORS exists.
Auto-approval approves code on your behalf. Each run searches the open PRs of those logins in
the same orgs or repos as your own (with neither set, in any repo) and approves the ones without a
live approval of yours: not drafts, not PRs with SKIP_LABEL, never your own. A push that dismisses
your approval, or a review where you requested changes, makes it approve again. That's why it waits
for CI by default and why an end date is a good idea: --auto-approve without one gives it a week.
Once it's past, it approves nothing and logs it once.
The log and state live in ~/.local/state/gh-pr-watcher/. While a PR stays conflicting, you
get reminded at most once an hour (right away if a new one shows up or the list changes): a single notification for all of them, which opens the PR if there is one or a
local page listing them if there are several. The state only remembers since when each conflict
has been open: deleting it is harmless. Reviews that request changes follow the same scheme (one
notification, or a page if several arrive in the same run), but only once per PR; the same goes for CI failing after an update. Each log keeps its last 2000 lines (from days to a month, depending on the frequency
and how many PRs are open); older ones are dropped on each run.
| Command | What it does |
|---|---|
install [options] |
Saves the config and schedules the run (launchd on macOS; on Linux it prints the crontab line). With a terminal and no options, it asks. |
run [--dry-run] |
Runs once now. With --dry-run it reports what it would do, without updating branches, notifying or saving state. |
list [--json] |
Your open PRs as a run sees them (same filters), the ones that need you first: conflict, changes requested, CI failing, behind, up to date. Shows state, CI, reviews, what the last run did, age and title. Changes nothing. --json gives one object per PR. |
status [--json] |
Config, whether the agent is loaded, the last run, the last error and the tail of the log. |
doctor [--json] |
Checks everything a run needs and says how to fix each problem. Changes nothing. |
log [N] |
The last N log lines (30 by default). |
config |
The config file path and contents. |
test-notify |
Sends a test notification. |
detect [--user LOGIN] |
JSON with accounts, orgs, repos with your PRs, dependencies, config and current install. Meant for agents. |
uninstall |
Removes the schedule. Config and log stay. |
version · help |
Version and help. |
A misspelled command suggests the right one (rub → run). Errors go to stderr prefixed with
error: and a non-zero exit code.
With any of these options (except --dry-run), install asks nothing: the options override your
existing config and anything missing comes from the defaults.
| Option | What it does |
|---|---|
--user LOGIN |
gh account to use (GH_USER). |
--owners "A B" |
Orgs or users to watch (OWNERS). "" = all your PRs. |
--repos "o/r o/r" |
Only these repos (REPOS); replaces --owners. |
--every 15m|30m|1h|2h|3h |
How often it runs (EVERY_MINUTES). --every-hours 1|2|3 still works. |
--drafts · --no-drafts |
Update drafts or not (INCLUDE_DRAFTS). |
--skip-label L · --no-skip-label |
Exclude PRs with label L or not (SKIP_LABEL). |
--notify-image PATH |
Notification image (NOTIFY_IMAGE). "" = the logo. |
--notify-changes-requested · --no-notify-changes-requested |
Notify when a review requests changes or not (NOTIFY_CHANGES_REQUESTED). |
--notify-ci-failed · --no-notify-ci-failed |
Notify when CI fails on a branch it updated or not (NOTIFY_CI_FAILED). |
--auto-approve "a b" · --no-auto-approve |
Approve the PRs of these logins, or stop (AUTO_APPROVE_AUTHORS). |
--auto-approve-for 1w|<N>d|forever |
Until when it approves (AUTO_APPROVE_UNTIL): 1w = today + 7 days; forever = no end date. Without it and with no date in the config, 1w. --auto-approve-until YYYY-MM-DD sets the date. |
--auto-approve-require-ci · --no-auto-approve-require-ci |
Approve only once CI passed, or regardless (AUTO_APPROVE_REQUIRE_CI). |
--yes |
Don't ask: use the existing config and the defaults. Useful to reschedule after editing the file. |
--dry-run |
Shows the config and the LaunchAgent it would write, without touching anything. On its own, it still asks the questions. |
gh pr-watcher doctorIt flags each problem with the command that fixes it and exits non-zero if something fails. It changes nothing and sends no notifications. It checks:
ghandjqinstalled, andterminal-notifier(as a warning only).- The config: misspelled keys and invalid values.
- The
ghsession, thereposcope, and that each owner and repo exists and your account sees it. - Auto-approval, when it's on: whose PRs and until when, with a warning once the end date passed.
- The schedule: that the LaunchAgent is loaded, that it points to a
ghthat still exists (brew sometimes moves it) and that its schedule matchesEVERY_MINUTES. - That it actually ran. No notification covers this: if launchd stops firing, there's no error.
doctorcompares the last run with the last scheduled time; if the machine was off at that time it doesn't flag it, and if it was asleep it waits for launchd to run it on wake. - Another watcher running at the same time, the last run's error and a corrupted state.
doctor --json returns the same for an agent: {ok, checks: [{id, status, message, fix}]}.
gh extension upgrade pr-watcher # the schedule keeps working, no need to reinstall
gh pr-watcher uninstall && gh extension remove pr-watcheruninstall keeps the config and the log; to start from scratch, also delete
~/.config/gh-pr-watcher and ~/.local/state/gh-pr-watcher.
tests/run_tests.sh # all the tests
tests/run_tests.sh conflict # only those whose name contains "conflict"The tests run the script against fakes of gh, terminal-notifier, osascript, launchctl and
sleep (in tests/bin/, placed first in PATH) that answer from JSON fixtures: no network, no
gh session, and nothing touched outside a temp folder. Only jq is needed. CI runs them on Linux
and macOS, plus shellcheck.
MIT.

