GitHub from the Terminal with the GitHub CLI: gh pr, gh issue, gh run, gh api and Extensions
The GitHub CLI (gh) is not a Git tool but a client for GitHub's API; with a token that has the right scopes you manage pull requests, issues, CI runs and releases from the terminal. We cover gh pr, gh issue, gh run, gh api, aliases and extensions, Bookstore's two-line flow from PR to auto-merge, and the GH_HOST and scope pitfalls.
Version note: The commands in this post were tested with GitHub CLI 2.104.0 and Git 2.56.1. gh commands that need a GitHub account were not run and their output is not shown; every output shown comes from an installation that is not logged in and from the commands' own --help text. GitHub features are taken from GitHub Docs as of 15 November 2026.
Guess First
You have just installed the GitHub CLI, whose command is gh, and you have not logged in yet. You are in the API repo of Bookstore, the online bookstore used as the shared example across this site's series. Which of the three commands below work?
$ gh --version
gh version 2.104.0 (2026-11-12)
https://github.com/cli/cli/releases/tag/v2.104.0
$ gh pr status
To get started with GitHub CLI, please run: gh auth login
Alternatively, populate the GH_TOKEN environment variable with a GitHub API authentication token.
$ gh extension list
To get started with GitHub CLI, please run: gh auth login
Alternatively, populate the GH_TOKEN environment variable with a GitHub API authentication token.
Only the first. It is expected that the pull request status needs an account; it is surprising that the local list of extensions needs one too. gh is not a Git tool but a client that talks to GitHub's API: almost every command wants a token and goes to GitHub with it. In this post we run pull requests, issues, CI and releases from the terminal, reach the API directly with gh api, and extend gh with aliases and extensions. Since no GitHub account was used in this lab, commands that need an account are given without output, with an explanation.
Installation, Login and Scopes
On Windows it is installed with winget install --id GitHub.cli --source winget; after installing you need to open a new Windows Terminal window, a new tab is not enough. Then:
# not run: needs a GitHub account
$ gh auth login
$ gh auth status
$ gh auth setup-git
gh auth login logs in in the browser with a one-time code and puts the token in the system credential store, Credential Manager on Windows; if there is no store it falls back to a plain text file, and gh auth status tells you where it is. gh auth setup-git registers gh as Git's credential helper, so git push uses the same token too. It is the same credential helper and token logic we saw in the post on connecting to GitHub. Without a login, gh auth status says:
$ gh auth status
You are not logged into any GitHub hosts. To log in, run: gh auth login
What a token can do is decided by its scopes. gh auth login --help says the minimum is repo, read:org and gist. If a job needs more, you extend the token:
$ gh auth refresh --help | grep -E -- '--(scopes|remove-scopes)[ =]'
-r, --remove-scopes strings Authentication scopes to remove from gh
-s, --scopes strings Additional authentication scopes for gh to have
$ gh auth refresh --scopes write:org,read:public_key
$ gh auth refresh --remove-scopes delete_repo
The one you run into most often is the workflow scope: a push that changes a file under .github/workflows/ is rejected without it. The fix is gh auth refresh -s workflow. If you have more than one account, like Ada in the git config post, gh auth login adds the second account and gh auth switch --user <account> moves between them.
Pull Requests: create, checkout, review, merge
The work on a feature branch is done. Every step of GitHub flow runs from the terminal:
# not run: needs a GitHub account
$ gh pr create --fill --draft
$ gh pr ready
$ gh pr checkout 42
$ gh pr review 42 --approve
$ gh pr merge 42 --squash --delete-branch
--fill fills in the title and the description from the commits; one more payoff for writing good commit messages. If the branch has not been pushed yet, gh asks where to push it. The options are in the local help text:
$ gh pr create --help | grep -E -- '--(fill|fill-first|draft)\['
-d, --draft[=true|false] Mark pull request as a draft
-f, --fill[=true|false] Use commit info for title and body
--fill-first[=true|false] Use first commit info for title and body
$ gh pr review --help | grep -E -- '--(approve|request-changes|comment)\['
-a, --approve[=true|false] Approve pull request
-c, --comment[=true|false] Comment on a pull request
-r, --request-changes[=true|false] Request changes on a pull request
$ gh pr merge --help | grep -E -- '--(auto|squash|delete-branch|disable-auto)'
--auto[=true|false] Automatically merge only after necessary requirements are met
-d, --delete-branch[=true|false] Delete the local and remote branch after merge
--disable-auto[=true|false] Disable auto-merge for this pull request
-s, --squash[=true|false] Squash the commits into one commit and merge it into the base branch
gh pr checkout 42 is the shortest way to try a teammate's PR locally: it fetches the branch, creates it locally and switches to it; it also works for PRs coming from forks. The merge methods are the same three we saw in the pull request post: --merge, --squash, --rebase.
Issues and Issue Templates
The same idea works for issues: gh issue create, gh issue list --label bug, gh issue view 17 --comments, gh issue close 17. So that bug reports in Bookstore always arrive with the same information, the repo has an issue form, .github/ISSUE_TEMPLATE/bug.yml:
name: Bug report
description: Something in Bookstore does not work as expected
labels: [bug]
body:
- type: input
id: page
attributes:
label: Page or endpoint
placeholder: /api/orders
validations:
required: true
- type: textarea
id: steps
attributes:
label: Steps to reproduce
validations:
required: true
GitHub's web interface shows this file as a form. In the terminal you can pick the template with --template; if you do not pass --title and --body, gh asks for what is missing:
$ gh issue create --help | grep -E -- '--(template|label|title|body) '
-b, --body string Supply a body. Will prompt for one otherwise.
-l, --label name Add labels by name
-T, --template name Template name to use as starting body text
-t, --title string Supply a title. Will prompt for one otherwise.
$ gh issue create --title "I found a bug" --body "Nothing works"
$ gh issue create --label "bug,help wanted"
$ gh issue create --label bug --label "help wanted"
$ gh issue create --template "Bug Report"
Following CI from the Terminal and Cutting a Release
After opening the PR, instead of refreshing the Actions tab in the browser:
# not run: needs a GitHub account
$ gh run list --branch feature/institutional-discount --limit 5
$ gh run watch --exit-status
$ gh run view --log-failed
$ gh run rerun --failed
$ gh workflow run ci.yml -f configuration=Debug
$ gh release create v1.2.0 --verify-tag --generate-notes
run watch shows a running run live, step by step; with --exit-status it also exits with a non-zero code if the run fails, so it can be used as a condition in a script. run view --log-failed fetches the log of the failed steps only. rerun --failed reruns only the failed jobs and the jobs they depend on; for a flaky test that is cheaper than running the whole matrix again. --debug turns on, for that rerun, the debug logging we saw in the debug, speed and cost post of the GitHub Actions series:
$ gh run rerun --help | grep -E -- '--(failed|debug|job)'
Note that due to historical reasons, the `--job` flag may not take what you expect.
However, this `<number>` should not be used with the `--job` flag and will result in the
-d, --debug[=true|false] Rerun with debug logging
--failed[=true|false] Rerun only failed jobs, including dependencies
-j, --job string Rerun a specific job ID from a run, including dependencies
gh workflow run starts a workflow that has a workflow_dispatch trigger and passes inputs with -f. Bookstore's CI has this trigger and a configuration input; we covered workflow_dispatch in detail in the triggers post of the GitHub Actions series:
on:
pull_request:
workflow_dispatch:
inputs:
configuration:
description: Build configuration
type: choice
options: [Release, Debug]
default: Release
We checked the whole file with actionlint:
$ actionlint -version | head -1
1.7.12
$ actionlint .github/workflows/ci.yml && echo actionlint-ok
actionlint-ok
gh release create v1.2.0 --verify-tag --generate-notes does the GitHub side of the work from the tags and releases post: --verify-tag stops if the tag does not exist on the remote, so you must have pushed the tag first, and --generate-notes produces the notes from the merged PRs through GitHub's Release Notes API.
gh api: REST and GraphQL
For everything that has no ready-made gh command there is gh api. It handles authentication, the host and pagination; the rest is GitHub's REST API, v3, or its GraphQL API, v4:
# not run: needs a GitHub account
$ gh api repos/{owner}/{repo}/pulls --jq '.[] | "\(.number) \(.title)"'
$ gh api --paginate repos/{owner}/{repo}/issues -f state=closed --method GET --jq '.[].number'
$ gh api graphql -f query='query { viewer { login } }' --jq '.data.viewer.login'
{owner} and {repo} are filled in from the remote of the repo you are in. --jq filters the response without needing a separate jq installation. -f and -F add parameters, and once you add a parameter the request switches to POST by default; do not forget to write --method GET for listings. --paginate fetches all the pages one after another. The matching options in the local help:
$ gh api --help | grep -E -- '--(jq|paginate|method|field|raw-field|hostname|slurp) '
`--method GET`.
-F, --field key=value Add a typed parameter in key=value format (use "@<path>" or "@-" to read value from file or stdin)
--hostname string The GitHub hostname for the request (default "github.com")
-q, --jq string Query to select values from the response using jq syntax
-X, --method string The HTTP method for the request (default "GET")
-f, --raw-field key=value Add a string parameter in key=value format
$ gh api repos/{owner}/{repo}/issues --jq '.[].title'
$ gh api graphql --paginate -f query='
$ gh api graphql --paginate --slurp -f query='
If you use gh inside GitHub Actions, you do not log in; you pass the workflow's token as the environment variable GH_TOKEN: ${{ github.token }}, and gh can do as much as the job's permissions: section allows.
Aliases and Extensions
To shorten commands you type often, there is gh alias:
$ gh alias set prs 'pr list --author "@me" --state open'
$ gh alias set --shell co-latest 'gh pr list --limit 1 --json number --jq ".[0].number" | xargs gh pr checkout'
$ gh alias list
co: pr checkout
co-latest: '!gh pr list --limit 1 --json number --jq ".[0].number" | xargs gh pr checkout'
prs: pr list --author "@me" --state open
Aliases are written to the local config and need no account. co is the only alias that comes built in. An alias defined with --shell runs as a shell command and can use pipes and other programs; it shows up in the list with a ! in front.
Extensions are separate repos that add new commands to gh: their names start with gh-, they are installed with gh extension install owner/gh-name and run as gh name. The help text gives a clear warning here: extensions are not verified, signed or endorsed by GitHub, and installing one means trusting its publisher. Before installing an extension, look at its source and at who publishes it, just as you would for an npm or NuGet package.
Bookstore: From PR to Auto-merge in Two Lines
Emre's fix for the institutional discount is ready. The whole flow is:
# not run: needs a GitHub account
$ gh pr create --fill --base main
$ gh pr checks --watch --fail-fast && gh pr merge --squash --auto --delete-branch
The first line opens the PR from the commits. The second line watches CI and stops at the first failed check; if everything is green, it puts auto-merge on the PR. --auto lets GitHub merge the PR by itself once the required reviews and checks are complete; Emre can close the terminal and move on to something else. There are two prerequisites: "Allow auto-merge" must be on in the repo settings, and main must have a rule, branch protection or a ruleset, that requires reviews or checks. The option only appears on PRs that cannot be merged right away. If someone without write access pushes a new commit to the branch, auto-merge turns itself off.
Common Pitfalls
The first is a token whose scopes are not enough. The error often arrives as "resource not accessible" or a rejected push, and does not say the problem is a scope. If you push workflow files you need workflow, if you read organization data you need read:org; gh auth status lists the token's scopes and gh auth refresh -s adds them. If you use a fine-grained token, gh auth login --help recommends passing it through GH_TOKEN rather than --with-token.
The second is forgetting GH_HOST on GitHub Enterprise Server. Inside a Git repo, gh works out the host from the remote. Outside a repo, for example when running gh api or gh repo clone owner/name, the default is github.com and the command goes to the wrong place. Companies with their own server need the GH_HOST environment variable or --hostname, and you log in separately for that host. On a host that is set but not logged in, gh auth status makes this clear:
$ GH_HOST=github.bookstore.example gh auth status
github.bookstore.example
X Failed to log in to github.bookstore.example using token (default)
...
- Error: Get "https://github.bookstore.example/api/v3/": dial tcp: lookup github.bookstore.example: no such host
In CI, the token for such a host is passed through GH_ENTERPRISE_TOKEN, not GH_TOKEN; gh help environment explains which variable is read for which host.
The third is taking --auto to mean "merge when checks pass" without setting up the prerequisites. If auto-merge is off in the repo or there are no required checks, the option does not help; the PR stays open and nobody notices.
The fourth is the request switching to POST once you pass parameters to gh api. To avoid creating a resource by mistake when you meant to list, write --method GET explicitly on read requests.
A Small Experiment
★☆☆ Define aliases for two commands you use often with gh alias set and look at them with gh alias list. Then read what GH_HOST, GH_REPO and GH_TOKEN do in the output of gh help environment.
★★☆ Create a test repo with your own account, add a workflow with a workflow_dispatch input, start it with gh workflow run and follow it with gh run watch --exit-status. Make one step fail on purpose and try gh run view --log-failed and gh run rerun --failed.
★★★ With gh api, list the last ten closed issues of a repo using --paginate and --jq, then fetch the same information with gh api graphql. Compare the size of the two responses and how many requests each made.
With this post we have reached the end of the series: from the first commit to preparing for Git 3.0, and from there to submodules, big repos, config, searching history and the GitHub CLI. Git does not stand still; the series will continue with new Git releases. When 2.98 comes out, when 3.0 arrives and after that, we will add new posts in the same "this is how we used to do it, this is how we do it now" style.