You are in the middle of coding, and you want to open a PR, check the status of a workflow or read the comments of an issue. Traditionally, you have to leave your terminal, open the browser, navigate to the right place.
GitHub CLI (gh) lets you do all of that without leaving the terminal.
Creating PRs, triggering workflows, handling issues: everything is reachable from
the command line.
What you will learn
- Install and authenticate GitHub CLI on your machine
- Handle Pull Requests: create, list, review, merge
- Drive GitHub Actions: trigger, follow and debug the runs
- Query the GitHub API with
gh apiandjq - Automate tasks inside your GitHub Actions workflows
What is GitHub CLI?
GitHub CLI is the official GitHub tool to interact with the platform from the terminal. It does not replace Git (which handles the code), but complements your workflow with the features specific to GitHub:
- Pull requests
- Issues
- GitHub Actions (workflows, runs)
- Releases
- Gists
- Repositories (creation, fork, clone)
- Codespaces
- And plenty more
Why use GitHub CLI?
The gain is not only comfort: every action becomes a reproducible command, so
it can be scripted and replayed identically inside a GitHub Actions workflow.
The table below puts the browser journey next to the equivalent gh command
for the four most frequent operations.
| Task | Without gh | With gh |
|---|---|---|
| Open a PR | Browser, New PR, fill the form | gh pr create |
| See the CI status | Actions tab, click the run | gh run list |
| Merge a PR | Browser, Merge button | gh pr merge |
| Trigger a workflow | Actions, Run workflow, click | gh workflow run |
The key benefits:
- Speed: no context switch
- Scriptable: automate it with shell scripts
- Consistency: the same workflow across all your projects
- CI integration: usable inside GitHub Actions workflows
Installation
gh is a single binary, with no dependency to install alongside it,
distributed by the package managers of every system. Pick the tab matching your
machine: the command installs the binary, its manual page and its shell
completion.
With Homebrew:
brew install ghDebian/Ubuntu:
# Add the GitHub repositorycurl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | sudo dd of=/usr/share/keyrings/githubcli-archive-keyring.gpgsudo chmod go+r /usr/share/keyrings/githubcli-archive-keyring.gpgecho "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | sudo tee /etc/apt/sources.list.d/github-cli.list > /dev/null
# Installsudo apt updatesudo apt install ghFedora/RHEL/CentOS:
sudo dnf install ghArch Linux:
sudo pacman -S github-cliWith Homebrew (any distribution):
brew install ghWith winget:
winget install GitHub.cliWith Chocolatey:
choco install ghWith Scoop:
scoop install ghChecking the installation:
gh --versionThe command must print the installed version. The official repository provides the latest stable version, for instance:
gh version 2.96.0 (2026-07-02)https://github.com/cli/cli/releases/tag/v2.96.0The distribution repositories (Debian, Ubuntu) often offer an older version: to get the latest commands, prefer the GitHub repository above or Homebrew.
Authentication
Before using gh, you have to authenticate against GitHub: every command goes
through the API, which refuses anonymous calls beyond a few requests per
hour. Authentication also defines the scopes of the token, so what gh is
allowed to do: a token without the workflow scope will read the runs but will
not be able to trigger any.
gh auth loginAn interactive wizard guides you:
-
Where do you want to authenticate?
- GitHub.com (the default option)
- GitHub Enterprise Server
-
Which protocol do you prefer for Git?
- HTTPS (recommended)
- SSH
-
How do you want to authenticate?
- Through the browser (the simplest)
- With a personal access token
If you pick "browser", gh opens a page where you grant the access.
Checking the authentication:
gh auth statusExample output:
github.com Logged in to github.com account your-username Git operations configured with https protocol Token: gho_************************************ Token scopes: gist, read:org, repo, workflowAuthentication in scripts
For scripts or CI, use a token: the browser flow assumes an interactive session,
which is impossible on a server. gh automatically reads the GH_TOKEN
variable and uses it without writing anything to disk, which suits a
disposable container.
# Through an environment variableexport GH_TOKEN=ghp_xxxxxxxxxxxxgh api user
# Or by passing the tokengh auth login --with-token < token.txtInside GitHub Actions, the GITHUB_TOKEN is available automatically:
- name: List the PRs run: gh pr list env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}Handling Pull Requests
The gh pr family covers the complete lifecycle of a Pull Request: creation,
consultation, review, local checkout and merge. All those commands infer the
repository and the current branch from the Git directory you are in,
which avoids repeating owner/repo on every call.
Creating a PR
gh pr create pushes the current branch if needed, then opens the Pull Request
against the default branch of the repository. Without --title or --body
the command goes interactive; --draft creates a draft, which does not
summon the reviewers until it is marked ready.
# Interactive: gh asks questionsgh pr create
# In one linegh pr create --title "Add feature X" --body "Description of the PR"
# With labels and reviewersgh pr create --title "Fix bug" --label "bug" --reviewer "alice,bob"
# A draft PRgh pr create --draft --title "WIP: New feature"Listing the PRs
gh pr list shows the open PRs of the current repository by default, limited
to the first thirty. The filters combine with each other, and --search accepts
the same search syntax as the web interface: a filter already proven in the
browser replays as it is on the command line.
# Open PRsgh pr list
# PRs with filtersgh pr list --state all # All of them (open, closed, merged)gh pr list --author "@me" # My PRsgh pr list --label "bug" # By labelgh pr list --search "is:open draft:false" # Advanced searchViewing a PR
Without a number, gh pr view targets the PR attached to the current branch
and prints its description, state and check results. The --json output
feeds a script or jq, while --web switches to the browser for the commented
diff.
# Show the detailsgh pr view 42
# In the browsergh pr view 42 --web
# As JSON (for scripts)gh pr view 42 --json title,state,reviewsChecking out a PR
Handy to test the code of a PR locally: gh pr checkout fetches the branch of
the PR, including when it comes from a fork, and configures the tracking of
the remote branch. You can then run the tests, add a commit and push it back to
the original branch if the author allowed maintainers to edit their PR.
gh pr checkout 42That creates a local branch holding the code of the PR.
Merging a PR
gh pr merge asks GitHub to merge, and GitHub applies the branch protection
rules: the command fails when a review or a required check is missing, it never
forces its way through. --squash and --rebase override the mode configured on
the repository for that one merge, and --delete-branch removes the remote
branch along with its local copy.
# Standard mergegh pr merge 42
# Squash mergegh pr merge 42 --squash
# Rebase mergegh pr merge 42 --rebase
# Delete the branch after the mergegh pr merge 42 --delete-branchReviewing a PR
gh pr review submits a formal review, not a plain comment: approval unlocks
the merge on a repository requiring reviewers, and --request-changes blocks it
until a new review lifts the request. GitHub refuses to let an author review
their own PR: in that case, only gh pr comment works.
# Approvegh pr review 42 --approve
# Request changesgh pr review 42 --request-changes --body "X should be fixed"
# Comment without approvinggh pr review 42 --comment --body "Good approach!"GitHub Actions with gh
gh is particularly useful to handle GitHub Actions workflows. Two families of
commands share the work: gh workflow handles the definitions (list, enable,
trigger) and gh run handles the runs (follow, read the logs, rerun,
cancel). Together, they replace the Actions tab for all the everyday
diagnosis.
Listing the workflows
gh workflow list enumerates the files of .github/workflows/ recognised by
GitHub, with their state (active or disabled) and their numeric
identifier. That identifier is the one to prefer in a script: it survives a
rename of the file, unlike the name of the workflow.
# See the workflows of the repositorygh workflow list
# Example output:# NAME STATE ID# CI active 12345678# Deploy active 12345679# Security Scan active 12345680Triggering a workflow
gh workflow run only works on a workflow declaring the workflow_dispatch
trigger: without it, GitHub rejects the call. The parameters declared in
inputs: are passed with -f key=value, and --ref picks the branch or the tag
the definition of the workflow will be read from.
# Trigger a manual workflow (workflow_dispatch)gh workflow run ci.yml
# With inputsgh workflow run deploy.yml -f environment=staging -f version=1.2.3
# On a specific branchgh workflow run ci.yml --ref feature-branchSeeing the runs
gh run list mixes every branch and every workflow by default, from the most
recent run to the oldest: on an active repository, the --workflow and
--status filters are indispensable to find anything. This is where you get the
identifier expected by gh run view, rerun and cancel.
# Latest runsgh run list
# Runs of one specific workflowgh run list --workflow ci.yml
# Runs in flightgh run list --status in_progress
# Failed runsgh run list --status failureThe details of a run
The --log option dumps the complete logs into the terminal, where grep
handles them far faster than scrolling through the web interface, and --job
narrows the output down to a single job. gh run watch follows a run in
flight and returns when it ends, with a non-zero exit code on failure.
# See one specific rungh run view 12345678
# See the logsgh run view 12345678 --log
# Logs of one specific jobgh run view 12345678 --log --job 98765432
# Follow it livegh run watch 12345678Rerunning a run
gh run rerun restarts the run on the same commit, which avoids the empty
courtesy commit. --failed only replays the failed jobs and their dependants,
a considerable gain on a wide matrix, and --debug enables the detailed logging
of the runner.
# Complete rerungh run rerun 12345678
# Rerun only the failed jobsgh run rerun 12345678 --failed
# Rerun with debug enabledgh run rerun 12345678 --debugCancelling a run
Cancellation is not instant: GitHub interrupts the jobs cleanly, and the
steps marked if: always() still run. The run ends with the cancelled status,
distinct from failure in the filters and the badges.
gh run cancel 12345678Downloading the artifacts
gh run download extracts every artifact of the run into a folder named
after it, -n fetching only one. Once the retention period configured on the
repository has passed, the artifacts are destroyed and the command fails.
# List the artifacts of a rungh run view 12345678 --json artifacts
# Download every artifactgh run download 12345678
# Download one specific artifactgh run download 12345678 -n build-outputHandling issues
The gh issue family follows the mechanics of gh pr: repository inferred from
the current directory, creation, filtering and updates. Its main benefit is to
open a ticket at the exact moment you notice the problem, without
interrupting what you were doing in the terminal.
Creating an issue
With no argument, gh issue create opens your editor for the body of the
ticket, then offers the labels and assignees defined on the repository. The
@me value designates the authenticated account, which avoids hard-coding an
identifier into a script.
# Interactivegh issue create
# In one linegh issue create --title "Bug: crash on login" --body "Description..."
# With labels and an assigneegh issue create --title "Feature request" --label "enhancement" --assignee "@me"Listing and filtering
Repeating --label applies a logical AND: the example below only returns the
tickets carrying both labels at once. For the criteria the options do not cover,
dates or free text, --search accepts the complete GitHub search syntax.
# Open issuesgh issue list
# My issuesgh issue list --assignee "@me"
# By labelgh issue list --label "bug" --label "priority:high"
# Searchgh issue list --search "is:open label:bug created:>2024-01-01"Managing an issue
All those commands take the number of the ticket as an argument. gh issue edit is the one that serves in automation: it adds or removes labels and
assignees without touching the title or the body, so without overwriting
someone else's work.
# Viewgh issue view 123
# Commentgh issue comment 123 --body "I am taking this bug!"
# Closegh issue close 123
# Reopengh issue reopen 123
# Editgh issue edit 123 --add-label "in-progress" --add-assignee "alice"Handling repositories
The gh repo family treats the repository as a GitHub object: creation,
fork, cloning and metadata lookup. It relies on git for the local
operations, but on top of that handles what git ignores entirely, such as the
visibility of the repository or the link between a fork and its origin.
Creating a repository
With --source=., gh repo create attaches the existing local Git repository to
the remote one it has just created, configuring the origin remote;
--push sends the current branch straight away. In non-interactive mode, the
visibility (--public or --private) is mandatory.
# Interactivegh repo create
# A new public repositorygh repo create my-project --public
# A new private repository with a descriptiongh repo create my-project --private --description "My great project"
# Create from the current directorygh repo create --source=. --public --pushCloning
gh repo clone accepts the short owner/repo notation and applies the
protocol chosen during gh auth login. On a repository you have forked, it
also adds the upstream remote towards the original, the step everyone
forgets when cloning with git and without which the fork never resyncs.
# Standard clonegh repo clone owner/repo
# Clone into a specific foldergh repo clone owner/repo ./my-folderForking
Run from an existing clone, the command offers to repoint the origin remote
towards your fork and to keep the original repository as upstream. Run
outside a repository, it simply creates the copy on your account.
# Fork into your accountgh repo fork owner/repo
# Fork and clonegh repo fork owner/repo --cloneViewing a repository
gh repo view renders the README straight into the terminal, along with the
description and the topics of the repository: enough to size up an unknown
project, a Marketplace action for instance, without opening a tab.
# Information about the current repositorygh repo view
# Information about another repositorygh repo view owner/repo
# In the browsergh repo view --webThe GitHub API with gh
gh can call any endpoint of the GitHub API: gh api reuses the token already
configured, sets the authentication and API version headers, then returns the raw
JSON. It is the escape hatch when a feature has no dedicated command, or when
you need a field gh pr view does not print. In exchange, you lose the
formatting and the default values: the repository has to be named explicitly in
the path.
Simple requests
The path passed to gh api is relative to https://api.github.com, no need
to repeat the domain, and the default method is GET. The answer is the
complete representation of the resource, often several dozen fields, hence the
filtering shown just after.
# Your profilegh api user
# One specific repositorygh api repos/owner/repo
# The PRs of a repositorygh api repos/owner/repo/pullsWith jq to filter
The --jq option applies a jq filter inside gh: the jq binary does not
have to be installed on the machine, which matters on a minimal runner. The
output becomes line-by-line text, directly consumable by a shell loop, instead of
the complete JSON.
# The names of the branchesgh api repos/owner/repo/branches --jq '.[].name'
# The title and state of the PRsgh api repos/owner/repo/pulls --jq '.[] | {title, state}'POST requests
As soon as a -f field is present, gh api automatically switches to POST:
no need to specify the method, -X only serves for PATCH or DELETE. Every
-f sends a string; for a number or a boolean, use -F, otherwise the API
rejects the request.
# Create a comment on an issuegh api repos/owner/repo/issues/123/comments \ -f body="A comment through the API"
# Create a labelgh api repos/owner/repo/labels \ -f name="priority:critical" \ -f color="FF0000"Pagination
The GitHub API answers in pages of 30 items by default, 100 at most: without
--paginate, a script counting the issues of a large repository silently misses
most of them. The option chains the calls and concatenates the pages, at the
cost of as many requests counted against your hourly quota.
# Every page of resultsgh api repos/owner/repo/issues --paginate --jq '.[].title'Using it inside GitHub Actions
gh is pre-installed on GitHub runners. Use it for automated tasks. It does not
authenticate itself for all that: every step calling it must receive
GH_TOKEN in its env: block, and the job must ask for the permission
matching the action (pull-requests: write to comment, contents: write to
publish a release). That combination replaces most third-party actions that only
make one API call.
Commenting on a PR automatically
This workflow posts a welcome message when a PR is opened. Two details make it
safe: the job asks for the only permission gh pr comment needs,
pull-requests: write, and the PR number travels through an env: block
instead of being interpolated into run:, which rules out any template
injection.
name: Comment on PR
on: pull_request: types: [opened]
# No rights by default: the job asks for the minimumpermissions: {}
jobs: comment: runs-on: ubuntu-24.04 permissions: pull-requests: write steps: - name: Add welcome comment env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} PR_NUMBER: ${{ github.event.pull_request.number }} run: | gh pr comment "$PR_NUMBER" \ --body "Thanks for this PR! The team will review it within 48h."Creating a release automatically
Triggered by pushing a v* tag, this workflow publishes the matching
release. The --generate-notes option asks GitHub to compose the release
notes from the PRs merged since the previous tag: no more changelog to maintain
by hand. The contents: write permission is mandatory here, since a release
modifies the repository.
name: Release
on: push: tags: ['v*']
# No rights by default: the job asks for the minimumpermissions: {}
jobs: release: runs-on: ubuntu-24.04 permissions: contents: write steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false
- name: Create GitHub Release env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} TAG: ${{ github.ref_name }} run: | gh release create "$TAG" \ --title "Release $TAG" \ --generate-notesTriggering a deployment workflow
This step chains two workflows: the one that builds triggers the one that
deploys, passing it the environment and the SHA to publish. The
GITHUB_TOKEN is enough, because workflow_dispatch is one of the rare
triggers insensitive to the anti-recursion protection of GitHub, which otherwise
prevents a workflow from triggering another. The called workflow must declare the
same inputs: as the ones passed with -f.
- name: Trigger deploy workflow run: | gh workflow run deploy.yml \ -f environment=production \ -f version=${{ github.sha }} env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}Waiting for a run to finish
This step blocks until the CI is done: the nested command fetches the identifier
of the most recent run of the targeted workflow, then gh run watch follows
it to its conclusion. The exit code is non-zero when the run fails, which makes
the calling step fail without writing a single waiting loop.
- name: Wait for checks to pass run: | gh run watch $(gh run list --workflow ci.yml --json databaseId --jq '.[0].databaseId') env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}gh extensions
gh supports extensions to widen its capabilities: they are GitHub
repositories whose name starts with gh-, which the CLI installs and exposes
as extra subcommands. They run with your token and your rights: treat them
like a Marketplace action, and look at who maintains them before installing.
Listing the available extensions
That command opens a text-mode interface and therefore requires an interactive
terminal: in a script or in CI, go straight to gh extension install with the
name of the repository.
gh extension browseInstalling an extension
The installation is done by repository, in the owner/name form, and the
subcommand becomes available immediately. Extensions do not update themselves:
gh extension upgrade --all takes care of that.
# The copilot extensiongh extension install github/gh-copilot
# The dashboard extensiongh extension install dlvhdr/gh-dashThe popular extensions
These four extensions cover the recurring needs: the daily PR review, command
line assistance, cleaning up merged branches and generating release notes. None
of them is maintained by GitHub, apart from gh-copilot: the caution
described above applies.
| Extension | Description |
|---|---|
gh-dash | An interactive dashboard for PRs and issues |
gh-copilot | An AI assistant in the terminal |
gh-poi | Cleans up the merged branches |
gh-changelog | Generates changelogs |
Configuration
gh reads its preferences from a single YAML file and offers gh config set
to modify it without editing it by hand. Two settings really change everyday
comfort: the editor opened to write the bodies of PRs and issues, and the
aliases, which shorten the commands you type ten times a day.
The configuration file
The configuration is stored in ~/.config/gh/config.yml: that file can be
versioned and copied as it is from one machine to another. It holds no
secret, the authentication token living separately in hosts.yml or in the
system keychain.
git_protocol: httpseditor: vimprompt: enabledpager: lessaliases: co: pr checkout prl: pr list prv: pr viewCustom aliases
Create shortcuts for the frequent commands: gh alias set writes the alias into
the configuration file, so it applies to all your repositories. The arguments
you pass to the alias are appended at the end of the expanded command, which
lets you write gh co 42. gh alias list prints the aliases already defined.
# Create an aliasgh alias set prl 'pr list'gh alias set co 'pr checkout'
# An alias with argumentsgh alias set prw 'pr view --web'
# Use the aliasgh prlgh co 42Changing the default editor
With no setting, gh follows the EDITOR environment variable. The --wait
option is mandatory with a graphical editor such as VS Code: without it, the
command returns immediately and gh gets an empty message body.
gh config set editor "code --wait"Troubleshooting
Almost every gh failure traces back to authentication: a missing token, an
expired one, or one whose scopes do not cover the requested action. The error
message of the API often stays generic, it does not name the missing scope. So
start systematically with gh auth status before looking elsewhere.
"permission denied" or "resource not accessible"
Symptom: gh cannot perform an action.
Fixes:
# Check the scopes of the tokengh auth status
# Re-authenticate with more permissionsgh auth refresh -s workflow,repo,admin:org"not logged in"
Symptom: gh asks for authentication.
Fix:
gh auth loginProxy problems
Behind a corporate proxy, gh follows the HTTPS_PROXY and NO_PROXY
variables like most network tools. If the proxy inspects the encrypted traffic,
it presents its own certificate: add its certificate authority to the trust
store of the system, otherwise every command fails on a TLS error rather than
on an authentication one.
# Configure the proxyexport HTTPS_PROXY=http://proxy.example.com:8080gh auth loginKey points
Efficiency
Manage GitHub without leaving the terminal: PRs, issues, workflows.
Scriptable
Automate it with shell scripts or inside your CI workflows.
The complete API
Reach the whole GitHub API with gh api.
Extensible
Add extensions for the advanced features.
The key points:
gh auth loginto authenticate (once and for all)gh prfor everything about Pull Requestsgh runandgh workflowfor GitHub Actionsgh apito reach any endpoint- Use aliases for your frequent commands
FAQ
These questions come up systematically during the install and the first uses, in
particular about the boundary between git and gh and about
authentication away from the workstation.
git manages your repository locally (commits, branches, history). gh talks to the GitHub platform: Pull Requests, issues, releases, GitHub Actions, API. The two complement each other: gh relies on git for the local operations.
Yes for the commands handling a local repository (gh pr checkout, gh repo clone): gh calls git in the background. Install git if it is not there already.
Use a token rather than the browser flow: gh auth login --with-token < token.txt, or set the GH_TOKEN environment variable. In a GitHub Actions workflow, GH_TOKEN: ${{ github.token }} is enough.
Sign every account in with gh auth login, then switch with gh auth switch. The gh auth status command shows the active account at any time.
Yes: gh auth login --hostname github.my-company.com targets an Enterprise Server instance instead of github.com.
Next steps
- actionlint: the linter validating the syntax of a workflow before
gh workflow runtriggers it for nothing.