Installing gh is straightforward on every platform, with one recurring trap: your operating
system’s default package repository often ships an old version, and gh moves quickly enough that
old matters.
This lesson covers the correct source per platform, how to check what you actually got, and the packaging problem that silently installs the wrong thing.
Check what you have first
Section titled “Check what you have first”gh --versionOutput:
gh version 2.98.0 (2026-08-20)https://github.com/cli/cli/releases/tag/v2.98.0If this prints a version more than a few months old, upgrade before following any tutorial — including this one. Commands and flags are added regularly, and “that flag does not exist” is almost always a version problem rather than a documentation error.
Linux: Debian and Ubuntu
Section titled “Linux: Debian and Ubuntu”GitHub publishes an apt repository. Using it rather than the distribution’s own package is the
difference between a current gh and one that may be a year behind.
sudo mkdir -p -m 755 /etc/apt/keyringssudo curl -fsSL -o /etc/apt/keyrings/githubcli-archive-keyring.gpg \ https://cli.github.com/packages/githubcli-archive-keyring.gpgsudo chmod go+r /etc/apt/keyrings/githubcli-archive-keyring.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" \ | sudo tee /etc/apt/sources.list.d/github-cli.list > /dev/null
sudo apt updatesudo apt install ghWhat it doesAdds GitHub's signing key and apt repository, then installs gh from it.
Why we run itUbuntu's own repository carries gh, but often an older release. Adding GitHub's repository means upgrades arrive through the normal apt process.
Expected resultKey download, then apt output installing gh from cli.github.com.
Confirm you got GitHub’s build rather than your distribution’s:
apt-cache policy ghOutput:
gh: Installed: 2.98.0 Candidate: 2.98.0 Version table: *** 2.98.0 500 500 https://cli.github.com/packages stable/main amd64 PackagesThe source line is what matters. If it names your distribution’s archive instead of
cli.github.com, the repository was not added successfully.
Linux: other distributions
Section titled “Linux: other distributions”Fedora, RHEL, CentOS — gh is available through dnf, and GitHub publishes an RPM repository
for the same currency reasons as above.
Arch — packaged in the official repositories and generally current.
Anything else — a static binary from the project’s releases page works everywhere. Download the
archive for your architecture, extract it, and put gh on your PATH. No package manager, and no
automatic upgrades, so you take responsibility for updating it.
Homebrew is the usual route:
brew install ghbrew upgrade ghMacPorts also carries it. Both stay reasonably current.
Windows
Section titled “Windows”Several options, all fine:
winget install --id GitHub.clichoco install ghscoop install ghwinget is built in on current Windows versions and is the path of least resistance. An MSI
installer is also published on the releases page for environments where package managers are
restricted.
If you work in WSL, install the Linux package inside your WSL distribution rather than relying on the Windows binary — the two have separate configuration and credential storage, and mixing them produces authentication confusion that is tedious to diagnose.
Upgrading
Section titled “Upgrading”| Platform | Command |
|---|---|
| Debian / Ubuntu | sudo apt update && sudo apt upgrade gh |
| Fedora / RHEL | sudo dnf upgrade gh |
| macOS | brew upgrade gh |
| Windows | winget upgrade GitHub.cli |
| Manual binary | Download and replace |
gh notifies you when a newer release is available, which is a useful nudge and easy to ignore for
months. Upgrading is cheap; do it when prompted.
Shell completion
Section titled “Shell completion”Worth setting up immediately. gh has many subcommands and flags, and completion converts
remembering them into pressing Tab.
# basheval "$(gh completion -s bash)"
# zsheval "$(gh completion -s zsh)"
# fishgh completion -s fish | source
# PowerShellgh completion -s powershell | Out-String | Invoke-ExpressionWhat it doesGenerates a completion script for your shell and loads it in the current session.
Why we run itCompletion covers subcommands, flags and in some cases values. It is the single change that most improves day-to-day CLI use.
Expected resultNo output when sourced successfully.
To make it permanent, add the appropriate line to your shell’s startup file — ~/.bashrc,
~/.zshrc, or your PowerShell profile.
Verifying the installation
Section titled “Verifying the installation”gh --version— confirmsghis on yourPATHand reports the release.gh --help— lists the command groups available in your version.gh auth status— confirms whether you are authenticated. Expect “not logged into any hosts” at this stage; that is the next lesson.gh repo view cli/cli— a read-only call against a public repository, which works without authentication and confirms network access.
Step 4 is worth running: it exercises the whole path from binary to API without needing credentials, which separates “gh is broken” from “authentication is not set up”.
Uninstalling
Section titled “Uninstalling”| Platform | Command |
|---|---|
| Debian / Ubuntu | sudo apt remove gh |
| Fedora / RHEL | sudo dnf remove gh |
| macOS | brew uninstall gh |
| Windows | winget uninstall GitHub.cli |
Removing the package leaves your configuration and stored credentials. To remove those as well, log
out first with gh auth logout, then delete ~/.config/gh/ — or the equivalent under
%APPDATA%\GitHub CLI on Windows.
Troubleshooting
Section titled “Troubleshooting”gh: command not found after installing. The binary is not on your PATH. Open a new shell, or
check where the package placed it with which gh or command -v gh.
A flag from documentation does not exist. Almost always an old version. Check gh --version and
compare against the current release.
apt installs an old version despite adding the repository. The keyring is corrupt — see the
warning above. Verify with apt-cache policy gh that the source is cli.github.com.
Certificate or proxy errors. gh respects the standard HTTPS_PROXY and NO_PROXY environment
variables. In a corporate environment with TLS interception, the proxy’s CA must be trusted by the
system store.
Two versions installed. Common when a manual binary was installed before the package. Check with
which -a gh and remove the one you do not want.
Keeping several machines consistent
Section titled “Keeping several machines consistent”Once you use gh on more than one machine, version drift becomes a real source of confusion — a
script that works on your laptop fails on a server because the server has a release from eighteen
months ago and the flag you used did not exist yet.
Check quickly across machines:
gh --version | head -1Two habits prevent most of the pain. Pin the expectation in your scripts where a command is version-sensitive:
required=2.60.0installed=$(gh --version | head -1 | awk '{print $3}')if [ "$(printf '%s\n%s\n' "$required" "$installed" | sort -V | head -1)" != "$required" ]; then echo "gh $required or newer required; found $installed" >&2 exit 1fisort -V compares version strings correctly, which string comparison does not — 2.9.0 sorts after
2.10.0 alphabetically and before it numerically.
And install from the same source everywhere. A mix of distribution packages, Homebrew and manual binaries across a team guarantees that “works on my machine” happens for reasons unrelated to the code.
Containers and CI images
Section titled “Containers and CI images”In a container, prefer the official package repository over a distribution package, for the same currency reason:
FROM ubuntu:24.04
RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates curl gnupg \ && mkdir -p -m 755 /etc/apt/keyrings \ && curl -fsSL -o /etc/apt/keyrings/githubcli-archive-keyring.gpg \ https://cli.github.com/packages/githubcli-archive-keyring.gpg \ && chmod go+r /etc/apt/keyrings/githubcli-archive-keyring.gpg \ && echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" \ > /etc/apt/sources.list.d/github-cli.list \ && apt-get update && apt-get install -y --no-install-recommends gh \ && rm -rf /var/lib/apt/lists/*Note the key is written straight to its file here too — the same null-byte problem applies inside a
RUN instruction, since it is still a shell.
On GitHub-hosted Actions runners, gh is pre-installed, so no installation step is needed. It
also picks up GH_TOKEN from the environment automatically, which is why workflow steps using gh
are usually a single run: line.
For self-hosted runners you are responsible for installing and updating it, and a runner with a stale
gh is a classic source of a workflow that works for everyone except on one machine.
Verifying what you downloaded
Section titled “Verifying what you downloaded”If you install the binary manually rather than through a package manager, verify it. The project publishes checksums with each release:
VERSION=2.98.0curl -fsSLO "https://github.com/cli/cli/releases/download/v${VERSION}/gh_${VERSION}_linux_amd64.tar.gz"curl -fsSLO "https://github.com/cli/cli/releases/download/v${VERSION}/gh_${VERSION}_checksums.txt"sha256sum --check --ignore-missing gh_${VERSION}_checksums.txtOutput:
gh_2.98.0_linux_amd64.tar.gz: OKA package manager does this for you through repository signing, which is the main practical argument for using one. Doing it by hand is fine; skipping it entirely means you are executing a downloaded binary with your credentials and no idea whether it is the one that was published.
Configuration after installing
Section titled “Configuration after installing”Two settings are worth applying immediately on any new machine:
gh config set git_protocol ssh # or https, but choose deliberatelygh config set editor "code --wait" # whatever you actually usegit_protocol determines the URL form gh repo clone produces and what gh auth login configures
for Git. Setting it once avoids a repository cloned over HTTPS on a machine where everything else
uses SSH, which then prompts for credentials at the worst moment.
The editor setting matters for gh pr create and gh issue create without --body, which open an
editor. The default is whatever $EDITOR says, and on a bare server that is often nano or nothing
at all.
Extensions
Section titled “Extensions”gh can be extended with third-party commands, installed and invoked like built-ins:
gh extension listgh extension install owner/gh-somethinggh extension upgrade --allgh extension remove somethingAn extension is an executable named gh-<name> that gh discovers — it can be written in any
language, and gh extension create scaffolds one.
They are genuinely useful for team-specific workflows: a gh deploy wrapping your release process, or
a gh standup summarising your week. Anything you would otherwise paste into a shared document as a
shell snippet is a candidate.
Portability across platforms
Section titled “Portability across platforms”If your scripts run on more than one operating system, a few differences bite — and none of them are
gh itself, which behaves consistently.
date arithmetic. GNU date -d '30 days ago' does not work on macOS, which uses BSD date -v:
days_ago() { if date -u -d '1 day ago' >/dev/null 2>&1; then date -u -d "$1 days ago" +%Y-%m-%dT%H:%M:%SZ # GNU else date -u -v-"$1"d +%Y-%m-%dT%H:%M:%SZ # BSD fi}base64 -w0. The wrap flag is GNU-only; BSD base64 rejects -w and does not wrap anyway.
mapfile and readarray. Bash 4+, so absent on the Bash 3.2 that ships with macOS. while read
is the portable form.
Line endings. Scripts edited on Windows and run under WSL fail with confusing errors if they carry carriage returns.
Where a script must be portable, testing on both platforms is the only reliable check — these failures are quiet and specific rather than obvious.
Common mistakes
Section titled “Common mistakes”Installing from the distribution’s default repository. Frequently outdated.
Corrupting the keyring with command substitution. Silent fallback to an old version.
Following a tutorial without checking your version. Missing flags are usually a version gap.
Mixing the Windows binary with WSL. Two configurations, two credential stores, endless confusion.
Skipping shell completion. The cheapest usability improvement available.
What you learned
Section titled “What you learned”- Distribution packages are often behind; GitHub’s own repositories keep
ghcurrent. - Binary keys must be written straight to disk — shell command substitution corrupts them and the failure is near-silent.
apt-cache policy ghconfirms which source a package actually came from.gh repo viewon a public repository tests the installation without authentication.- WSL needs the Linux package, not the Windows one.
- A missing flag almost always means an old version.
Keeping it current
Section titled “Keeping it current”gh releases frequently and gains commands and flags with each release. Two habits avoid the
version-gap problems this lesson opened with.
Upgrade when prompted — it takes seconds through a package manager and prevents the “that flag does not exist” confusion that costs far longer to diagnose.
And check gh --version before following any tutorial, this one included. A documented command that
does not work is almost always a version difference rather than an error in what you read, and
knowing that saves you from debugging your own setup.
Pinning gh in CI
Section titled “Pinning gh in CI”GitHub-hosted runners ship gh pre-installed, at whatever version the runner image carries. That
version changes when the image does, which is a source of workflows that break without anyone
changing them.
For anything depending on a recent flag, assert the version rather than hoping:
- name: Check the gh version run: | required=2.60.0 installed=$(gh --version | head -1 | awk '{print $3}') if [ "$(printf '%s\n%s\n' "$required" "$installed" | sort -V | head -1)" != "$required" ]; then echo "::error::gh $required or newer required; runner has $installed" exit 1 fi::error:: makes the failure appear as an annotation rather than being buried in the log, which
matters when someone is looking at a red run and trying to work out why.
For a self-hosted runner, or where you need a specific version regardless of the image, install it
explicitly in the workflow using the apt steps from earlier in this lesson — the same null-byte
caution applies, since a run: block is still a shell.
The general principle: a tool your automation depends on should have its version asserted somewhere. Silent version drift produces failures that correlate with nothing you changed, which are the most expensive kind to diagnose.
Related lessons
Section titled “Related lessons”Check your understanding
4 questions — each one asks you to predict what Git or GitHub will do, not to recall a flag.