gh pr is the largest command family in the CLI, and the one that changes daily work most.
Everything in the Pull Requests cluster has a command here. This lesson covers the operations worth knowing and the output fields that make them scriptable.
The subcommands
Section titled “The subcommands”| Command | Does |
|---|---|
create | Open a pull request |
list | List pull requests |
status | Show pull requests relevant to you |
view | Show one, or fields of it |
diff | Show the diff |
checkout | Check the branch out locally |
checks | Show CI status |
review | Approve, request changes or comment |
comment | Add a comment |
edit | Change title, body, reviewers, labels |
ready | Mark a draft ready, or --undo |
update-branch | Bring the branch up to date with base |
merge | Merge, squash or rebase |
close / reopen | Close without merging, or reopen |
revert | Revert a merged pull request |
lock / unlock | Control the conversation |
Most commands take a number, a URL, or a branch name. Inside a checkout with a branch that has a pull request, they take nothing at all and operate on the current branch — convenient interactively, and worth being explicit about in scripts.
Creating
Section titled “Creating”gh pr create --base main --title "Add retry handling" --body "Closes #42"gh pr create --fillgh pr create --draft --title "Spike: alternative approach" --body "Seeking direction"gh pr create --repo OWNER/REPO --base main --head you:my-feature --fill--fill takes the title and body from your commits, which rewards
good commit messages and bypasses any
pull request template.
The last form is the cross-repository case: --head you:my-feature names the fork owner as well as
the branch, which is required when the head branch is not in the target repository.
Finding work
Section titled “Finding work”gh pr statusgh pr list --state open --limit 30gh pr list --author "@me"gh pr list --search "review-requested:@me"gh pr list --label bug --state opengh pr list --json number,title,author,createdAt --jq '.[] | "\(.number)\t\(.author.login)\t\(.title)"'gh pr status is the single most useful interactive command in the family: it shows what you have
open, what is waiting on you, and what is relevant to your branch, in one screen.
review-requested:@me is worth an alias. It is a far more reliable review queue than notifications.
Reading a pull request
Section titled “Reading a pull request”gh pr view PULL_NUMBER --json mergeable,mergeStateStatus,reviewDecision,statusCheckRollupWhat it doesFetches the fields that determine whether a pull request can merge, and why.
Why we run itThese three fields answer different questions and frequently disagree. Reading them together is how you diagnose 'approved but the button is disabled'.
Expected resultA JSON object; mergeStateStatus is usually the one explaining a block.
Other useful views:
gh pr view PULL_NUMBERgh pr view PULL_NUMBER --commentsgh pr diff PULL_NUMBERgh pr diff PULL_NUMBER --name-onlygh pr view PULL_NUMBER --json files --jq '.files[] | "\(.additions)+ \(.deletions)- \(.path)"'--name-only is the quick way to judge scope before committing to a review — a pull request touching
forty files across six directories is a different proposition from one touching three.
Reviewing
Section titled “Reviewing”gh pr checkout PULL_NUMBERgh pr review PULL_NUMBER --approve --body "Looks good."gh pr review PULL_NUMBER --request-changes --body "The retry loop can spin unbounded."gh pr review PULL_NUMBER --comment --body "One question, no objection."gh pr comment PULL_NUMBER --body "Deployed to staging for testing."gh pr checkout works for pull requests from forks too, which is its most useful property — it
handles the remote configuration that would otherwise be several manual steps.
Remember that --comment records no verdict; see
Pull Request Reviews.
Checks
Section titled “Checks”gh pr checksgh pr checks --watchgh pr checks --requiredgh pr checks --json name,state,link --jq '.[] | select(.state != "SUCCESS")'--watch blocks until checks complete, which makes it usable in a script that should wait before
merging. --required filters to the checks that actually gate the merge — useful on repositories
with many advisory checks.
Keeping current and merging
Section titled “Keeping current and merging”gh pr update-branch PULL_NUMBERgh pr ready PULL_NUMBERgh pr merge PULL_NUMBER --squash --delete-branchgh pr merge PULL_NUMBER --mergegh pr merge PULL_NUMBER --rebasegh pr merge PULL_NUMBER --auto --squash--auto enables auto-merge: the pull request merges by itself once every requirement is satisfied.
On a repository with slow checks this removes a great deal of waiting-and-returning, and it is
GitHub doing the merge rather than a script polling.
On a repository with a merge queue, gh pr merge enqueues
rather than merging.
Scripting patterns
Section titled “Scripting patterns”Two that come up constantly.
Merge every approved, passing, non-draft pull request:
gh pr list --state open --json number,isDraft,reviewDecision,statusCheckRollup \ --jq '.[] | select(.isDraft | not) | select(.reviewDecision == "APPROVED") | select([.statusCheckRollup[].state] | all(. == "SUCCESS")) | .number' \| while read -r pr; do echo "merging #$pr" gh pr merge "$pr" --squash --delete-branch doneReport pull requests waiting on review for more than two days:
gh pr list --state open --json number,title,createdAt,reviewDecision \ --jq --arg cutoff "$(date -u -d '2 days ago' +%Y-%m-%dT%H:%M:%SZ)" \ '.[] | select(.reviewDecision == null) | select(.createdAt < $cutoff) | "\(.number)\t\(.title)"'Both rely entirely on --json and --jq. Neither would survive a change to gh’s human-readable
table format, which is precisely the point.
Reviewing without leaving the terminal
Section titled “Reviewing without leaving the terminal”A complete review cycle, entirely in the shell:
# What is waiting on me?gh pr list --search "review-requested:@me" --json number,title,author \ --jq '.[] | [.number, .author.login, .title] | @tsv'
# How big is it?gh pr view 128 --json additions,deletions,changedFiles \ --jq '"\(.changedFiles) files, +\(.additions) -\(.deletions)"'
# What changed, at a glancegh pr diff 128 --name-only
# Read it properlygh pr diff 128
# Run itgh pr checkout 128# ...build, test, poke at it...
# Respondgh pr review 128 --request-changes --body "The retry loop can spin unbounded; see the comment on line 42."
# Return to where you weregit switch -The size check before reading is worth the two seconds. A pull request with four files and sixty lines is a different commitment from one with forty files and two thousand, and knowing which you are about to open changes whether you do it now or schedule it.
gh pr checkout is what makes terminal review genuinely competitive with the web interface. Reading a
diff tells you what changed; running the branch tells you whether it works, and some categories of
problem — behaviour under real input, performance, integration — are invisible in a diff.
Comments on specific lines
Section titled “Comments on specific lines”gh pr comment adds a conversation comment, not a line comment. Line comments need the API:
gh api --method POST repos/OWNER/REPO/pulls/128/comments \ -f body="This can spin unbounded when the server keeps closing the connection." \ -f commit_id="$(gh pr view 128 --json headRefOid --jq .headRefOid)" \ -f path="src/client.py" \ -F line=42 \ -f side=RIGHTside=RIGHT comments on the new version of the line; LEFT comments on the old one. line is the
line number in the diff’s target file, and commit_id must be the current head — which is why it is
fetched rather than hardcoded, since a comment against a stale commit is rejected.
Suggested changes are line comments with a specially formatted body:
gh api --method POST repos/OWNER/REPO/pulls/128/comments \ -f path="src/client.py" -F line=42 -f side=RIGHT \ -f commit_id="$(gh pr view 128 --json headRefOid --jq .headRefOid)" \ -f body='Cap the delay so this cannot grow without bound.
```suggestion delay = min(base_delay * (2 ** attempt), MAX_BACKOFF)```'That is more ceremony than clicking, and it is the primitive any review automation ultimately uses.
Understanding a blocked pull request
Section titled “Understanding a blocked pull request”The single most common question — “why can I not merge this?” — has a one-command answer:
gh pr view 128 --json mergeable,mergeStateStatus,reviewDecision,statusCheckRollup \ --jq '{ git: .mergeable, policy: .mergeStateStatus, review: .reviewDecision, failing: [.statusCheckRollup[] | select(.conclusion != "SUCCESS" and .conclusion != null) | .name] }'Output:
{ "failing": ["integration-tests"], "git": "MERGEABLE", "policy": "BLOCKED", "review": "APPROVED"}Read together: Git can merge it, review approved it, and one check is failing. Each field alone would have been misleading — which is exactly why “it is approved, why is the button greyed out” is such a persistent question.
The mergeStateStatus values are worth memorising: CLEAN is ready, BLOCKED means a policy
requirement is unmet, BEHIND means the base moved, DIRTY means conflicts, and UNSTABLE means
something non-required failed. UNSTABLE is mergeable — and habitually merging through it is how
non-required checks become decorative.
Auto-merge
Section titled “Auto-merge”On a repository with slow checks, --auto removes a great deal of waiting:
gh pr merge 128 --squash --auto --delete-branchThe pull request merges by itself once every requirement is satisfied. GitHub performs the merge, so there is no script polling and nothing to keep running.
Two conditions: auto-merge must be enabled on the repository, and the pull request must have at least one unmet requirement — enabling it on something already mergeable just merges immediately.
It pairs well with --delete-branch, giving a genuine fire-and-forget: request review, enable
auto-merge, and the change lands when it is ready without another visit.
Common mistakes
Section titled “Common mistakes”Using --comment when you mean approve or request changes. Records no state.
Relying on the current branch in scripts. Be explicit with a number and --repo.
Forgetting the owner prefix in --head for forks. GitHub looks in the wrong repository.
Parsing gh pr list output. Use --json.
Bulk-merging without a dry run. Test with echo.
Assuming gh pr merge merges on a queue-enabled branch. It enqueues.
A daily workflow
Section titled “A daily workflow”The commands become a habit when they fit into one sequence. A morning routine:
# What needs my attention?gh pr status
# Reviews waiting on me, oldest firstgh pr list --search "review-requested:@me sort:created-asc" \ --json number,title,author,createdAt \ --jq '.[] | [.number, .author.login, .title] | @tsv'
# My own open pull requests and their stategh pr list --author "@me" --json number,title,reviewDecision,mergeStateStatus \ --jq '.[] | [.number, (.reviewDecision // "PENDING"), .mergeStateStatus, .title] | @tsv'The third is the useful one. It tells you, in one line each, which of your changes are approved and merge-ready, which are blocked, and which nobody has looked at — so you know whether to chase a review, fix a check, or merge.
Worth turning into aliases:
gh alias set mine 'pr list --author "@me" --json number,title,reviewDecision,mergeStateStatus --jq ".[] | [.number, (.reviewDecision // \"PENDING\"), .mergeStateStatus, .title] | @tsv"'gh alias set todo 'pr list --search "review-requested:@me sort:created-asc"'Working with pull requests from forks
Section titled “Working with pull requests from forks”Contributions arrive from forks, and the commands handle it with one adjustment each.
# Check out a contributor's branch — handles the remote configuration for yough pr checkout 128
# See where it came fromgh pr view 128 --json headRepositoryOwner,headRefName,isCrossRepository \ --jq '{fork: .isCrossRepository, owner: .headRepositoryOwner.login, branch: .headRefName}'
# Push a fix to their branch, if they allowed maintainer editsgit commit -am "Fix the failing test"git pushgh pr checkout is what makes reviewing fork contributions practical. Doing it by hand means adding
a remote, fetching, and creating a tracking branch; gh resolves it from the pull request.
Whether you can push back depends on the contributor leaving allow edits from maintainers enabled. When they have, fixing a small problem yourself is usually kinder than a review round trip — but it is their branch, so say what you changed.
Bulk operations
Section titled “Bulk operations”Anything touching many pull requests needs the same care as bulk Issue operations.
# Everything approved, passing and not draftgh pr list --state open --json number,isDraft,reviewDecision,mergeStateStatus \ --jq '.[] | select(.isDraft | not) | select(.reviewDecision == "APPROVED") | select(.mergeStateStatus == "CLEAN") | .number'
# Stale — no update in three weeksgh pr list --state open --json number,title,updatedAt \ --jq --arg cutoff "$(date -u -d '21 days ago' +%Y-%m-%dT%H:%M:%SZ)" \ '.[] | select(.updatedAt < $cutoff) | [.number, .title] | @tsv'
# Behind the base branchgh pr list --state open --json number,mergeStateStatus \ --jq '.[] | select(.mergeStateStatus == "BEHIND") | .number'Each is a report before it is an action. Run them, read the output, and only then decide whether to
act on the list — with echo substituted for the real command until the list is exactly right.
The CLEAN filter in the first is doing important work: it means every policy requirement is
satisfied, so the script is not reimplementing your repository’s rules and getting them subtly wrong.
Exercise
Section titled “Exercise”- Create a branch, push it, and open a pull request with
gh pr create --fill. - Run
gh pr statusand find it. - Inspect it with
gh pr view --json mergeable,mergeStateStatus,reviewDecision. - Run
gh pr checks --watchand observe it block until checks finish. - Write the “waiting on review” query above and run it against a repository you work in.
- Merge with
gh pr merge --squash --delete-branch, then confirm the branch is gone withgit fetch --prune.
JSON fields worth knowing
Section titled “JSON fields worth knowing”gh pr view --json and gh pr list --json expose a large field set. The ones that answer real
questions:
| Field | Answers |
|---|---|
mergeable | Can Git combine the branches? |
mergeStateStatus | Does policy permit merging? |
reviewDecision | APPROVED, CHANGES_REQUESTED, REVIEW_REQUIRED, or null |
statusCheckRollup | Every check and its conclusion |
isDraft | Ready or not |
isCrossRepository | From a fork? |
headRefOid | The head commit SHA — needed for guarded merges |
reviewRequests | Who has been asked |
additions / deletions / changedFiles | Size, for triage |
autoMergeRequest | Is auto-merge enabled, and with what method |
Run gh pr view --json with no value for the full list on your version.
Combining them produces a single view that explains any pull request’s state:
gh pr view 128 --json number,title,isDraft,reviewDecision,mergeStateStatus,statusCheckRollup,headRefOid \ --jq '{ pr: .number, title: .title, draft: .isDraft, review: (.reviewDecision // "PENDING"), policy: .mergeStateStatus, failing: [.statusCheckRollup[] | select(.conclusion == "FAILURE") | .name], head: .headRefOid[0:7] }'Common flags worth remembering
Section titled “Common flags worth remembering”Scattered across subcommands, and easy to miss:
gh pr list --limit 200 # default is 30gh pr list --state all # open, closed and mergedgh pr view --web # open in a browsergh pr diff --color always | less -R # paged diff with colour preservedgh pr checks --required # only checks that gate the mergegh pr merge --admin # bypass, if you have permissiongh pr create --dry-run # show what would be created--admin deserves care. It bypasses branch protection where your permissions allow, which is
occasionally the correct emergency action and should never be routine — every use is a policy
requirement being skipped, and it is recorded as such.
--dry-run on gh pr create is worth knowing: it prints the title, body and target branches without
opening anything, which catches a wrong base branch before it becomes a pull request you have to
close.
What you learned
Section titled “What you learned”gh pr statusis the fastest interactive overview of what needs your attention.mergeable,mergeStateStatusandreviewDecisiontogether explain any blocked merge.gh pr checkouthandles fork remotes automatically.--watchblocks until checks finish;--autolets GitHub merge when requirements are met.- Scripted workflows depend on
--jsonand--jq, never on table output. - Bulk operations should be dry-run with
echobefore they act.
The short version
Section titled “The short version”Three commands do most of the work: gh pr status for what needs your attention, gh pr checkout for
reviewing something properly, and gh pr view --json mergeable,mergeStateStatus,reviewDecision for
understanding why something will not merge.
For anything scripted, use --json and --jq. The human-readable table changes between releases and
formats differently when output is not a terminal — so a script tested interactively can behave
differently in CI, which is a genuinely unpleasant class of bug.
Reverting a merged pull request
Section titled “Reverting a merged pull request”gh pr revert deserves a demonstration rather than a table entry, because reverting by hand is
error-prone in a specific way.
gh pr revert 128gh pr revert 128 --title "Revert: retry handling" --body "Caused a regression in reconnect handling."gh pr revert 128 --draftIt creates a new pull request containing the inverse of the merge. That is safer than reverting
locally for two reasons: it gets the parent selection right on a merge commit, which git revert -m
requires you to specify and which is easy to get wrong; and the revert goes through review and CI like
any other change rather than being pushed directly.
The --draft form is useful during an incident — open the revert immediately so it exists and is
visible, then mark it ready once someone has confirmed it is the correct response.