Skip to content

gh auth: Authenticating the GitHub CLI

Lesson 2 of 10Intermediate11 min readGitHub Engineering · GitHub CLIVerified: gh 2.98.0 on Ubuntu 24.04, August 2026

gh auth manages the credentials gh uses, and — optionally — the credentials Git uses for pushing and pulling.

Those are two separate things, and gh auth login can configure both. Understanding which is which prevents a specific confusion: gh working perfectly while git push asks for a password.

CommandPurpose
gh auth loginAuthenticate to a host
gh auth statusShow active account and state per host
gh auth switchChange the active account
gh auth logoutRemove stored credentials
gh auth refreshChange the scopes on an existing credential
gh auth tokenPrint the token gh is using
Terminal window
gh auth login

The prompts ask four things, and each is worth understanding rather than accepting.

Which host. github.com, or a GitHub Enterprise Server hostname. gh stores credentials per host, so you can be authenticated to several simultaneously.

Git protocol — HTTPS or SSH. This decides what gh configures for Git operations, and what URL form gh repo clone produces. HTTPS with the credential helper is the simpler path; SSH suits a machine where you already have keys. The trade-offs are in Account Setup.

Whether to authenticate Git with your GitHub credentials. Saying yes configures Git’s credential helper to use gh, so git push works without a separate token. This is the step that, skipped, produces the “gh works but git does not” confusion.

How to authenticate — browser or token. The browser flow is the default and is preferable interactively: it produces a credential scoped by gh and never exposes a token you might paste somewhere.

Terminal window
gh auth status

What it doesReports, per host, which account is active, how the credential is stored, the Git protocol, and the token's scopes.

Why we run itThis is the first diagnostic for anything authentication-related. Most 'gh is broken' reports are a wrong active account or a missing scope, and both are visible here.

Expected resultOne block per host, with a tick for a valid credential.

Output:

github.com
✓ Logged in to github.com account octocat (keyring)
- Active account: true
- Git operations protocol: ssh
- Token scopes: 'gist', 'read:org', 'repo', 'workflow'

Two lines matter most. Active account determines what every subsequent command does. Token scopes determine what is permitted — a missing scope produces a 403 that looks like a permissions problem on the repository rather than on your credential.

Work and personal accounts on one machine is common, and gh handles it directly.

Terminal window
gh auth login # log in to the second account
gh auth status # both listed; one marked active
gh auth switch # interactive picker
gh auth switch --user other-name # explicit

The active account applies to every gh command, which is a global mode with the usual hazard: running a command believing you are one identity when you are another. Before anything consequential in a shared or unfamiliar context, check gh auth status.

Tokens carry scopes. If a command fails with a permissions error despite you having repository access, the credential probably lacks the scope rather than you lacking the right.

Terminal window
gh auth refresh --scopes repo,workflow,read:org
gh auth refresh --scopes admin:org

gh auth refresh re-authorises the existing credential with a new scope set rather than creating a second login.

A common case: workflow files. Pushing a change to .github/workflows/ requires the workflow scope, and without it the push is rejected with a message about workflow permissions that reads like a repository policy problem.

Interactive login is wrong for CI: there is no browser, and a stored credential on a shared runner is a liability. Use an environment variable instead.

gh reads GH_TOKEN, and falls back to GITHUB_TOKEN:

Terminal window
export GH_TOKEN="$MY_SECRET_TOKEN"
gh pr list --repo OWNER/REPO

When the variable is set, gh uses it and ignores stored credentials entirely. Nothing is written to disk, which is exactly what you want on a machine you do not control.

In GitHub Actions, the workflow token is provided automatically and needs only to be passed through:

- name: Comment on the pull request
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: gh pr comment "$PR_NUMBER" --body "Build finished."

Non-interactive login from a token on stdin is also possible, and is preferable to putting it in an argument:

Terminal window
echo "$MY_TOKEN" | gh auth login --with-token

Prints the token gh is using, which is useful for handing credentials to another tool:

Terminal window
gh api repos/OWNER/REPO -H "Authorization: Bearer $(gh auth token)"

Treat its output as a live credential. Piping it into a log, a file, or a command that prints its arguments is the same as publishing it.

gh prefers your operating system’s keyring — Keychain on macOS, Secret Service on Linux, Credential Manager on Windows. gh auth status reports which store is in use.

Where no keyring is available, as on many headless Linux servers, gh falls back to a plain file under ~/.config/gh/. That file is readable by anything running as your user, which is a good reason to prefer GH_TOKEN from a secret store on servers rather than a stored login.

Terminal window
gh auth logout
gh auth logout --hostname github.com --user octocat

This removes the local credential. It does not revoke the token server-side — for a credential that may have been exposed, revoke it in your account settings as well.

SituationMethod
Your own workstationgh auth login, browser flow
Second account on the same machinegh auth login again, then gh auth switch
GitHub ActionsGH_TOKEN from the workflow token
Other CIGH_TOKEN from a secret store
Shared or headless serverGH_TOKEN per invocation; avoid stored credentials
Long-lived organisation automationA GitHub App, not a personal token

The last row matters more as automation grows. A personal token ties automation to a person, expires with their account, and carries their permissions. Fine-grained tokens and GitHub Apps cover the alternatives.

Almost every gh failure that looks mysterious is one of five things. In the order worth checking:

1. Which account is active?

Terminal window
gh auth status

With two accounts configured, the active one applies to every command. Running a work command while your personal account is active produces 404s on repositories you can definitely see in a browser — because that account cannot see them.

2. Does the token have the scope?

The scopes line in gh auth status is the answer. Pushing a change to .github/workflows/ without the workflow scope fails with a message about workflow permissions that reads like a repository policy problem and is not.

3. Is it a 404 that means 403?

GitHub returns “not found” for private resources your credential cannot see. While debugging, treat 404 on something you believe exists as “wrong account or insufficient scope” first.

4. Is GH_TOKEN set and overriding your login?

Terminal window
env | grep -E '^GH_TOKEN=|^GITHUB_TOKEN=' | sed 's/=.*/=<set>/'

An exported token in your shell profile silently takes precedence over gh auth login, which produces the confusing situation where gh auth status shows one account and commands behave as another. The command above confirms whether one is set without printing it.

5. Is the credential still valid?

Terminal window
gh api user --jq '.login'

A 401 means expired, revoked, or malformed. Tokens with an expiry do expire, and the failure arrives without warning.

gh works against GitHub Enterprise Server, with credentials stored per host:

Terminal window
gh auth login --hostname github.company.com
gh auth status
gh repo list --hostname github.company.com
export GH_HOST=github.company.com

Setting GH_HOST makes a shell session default to the enterprise instance, which is less error-prone than remembering --hostname on every command. Note that API paths and feature availability can differ from github.com — Enterprise Server runs a versioned release that lags the hosted product, so a command working against github.com may not exist there.

Classic token scopes are coarse, and a few come up constantly:

ScopeGrants
repoFull control of all repositories the user can reach, public and private
read:orgRead organisation membership and teams
workflowUpdate workflow files under .github/workflows/
gistCreate and edit gists
delete_repoDelete repositories
admin:orgFull organisation control

repo is the one to be wary of. It cannot be narrowed to a single repository and it includes write access — so a token created for a read-only reporting script can, if leaked, push to everything its owner can push to.

gh auth login requests a reasonable default set. Add scopes as you need them:

Terminal window
gh auth refresh --scopes repo,workflow,read:org

This is also why fine-grained tokens exist, and why they are preferable for anything scripted: they scope per repository and per permission rather than per account-wide capability.

gh can manage the SSH keys on your account, which removes a browser round trip when setting up a new machine:

Terminal window
gh ssh-key list
gh ssh-key add ~/.ssh/id_ed25519.pub --title "work laptop"
gh ssh-key delete KEY_ID
gh gpg-key list

gh auth login also offers to generate and upload a key during setup, which is the fastest path on a fresh machine — one command instead of ssh-keygen, copying the public half, and pasting it into settings.

Titles matter more than they seem. A key list full of “id_ed25519” tells you nothing when you are trying to work out which of six entries belongs to a laptop you no longer own, and unaccounted-for keys are unaccounted-for access.

Declining Git credential configuration during login. gh works, git push does not.

Not knowing which account is active. Run gh auth status before anything consequential.

Diagnosing a scope problem as a permissions problem. Check the scopes line first.

Storing credentials on a shared server. Use GH_TOKEN from a secret store.

Tokens in command arguments. Captured in history and visible to other processes.

Assuming logout revokes the token. It removes it locally only.

Using a personal token for team automation. It belongs to a person, not the team.

gh auth token prints a live credential. A few habits keep it from leaking.

Never assign it to a variable that gets echoed. Command substitution into a command that logs its arguments defeats the purpose:

Terminal window
# Fine — the token goes straight into the header
curl -H "Authorization: Bearer $(gh auth token)" https://api.github.com/user
# Dangerous — visible in the process list, and in `set -x` output
TOKEN=$(gh auth token)
some-tool --token "$TOKEN"

Beware set -x. Shell tracing prints every command with its arguments expanded, including tokens. Disable tracing around anything handling credentials:

Terminal window
set +x
export GH_TOKEN=$(gh auth token)
set -x

Check what your shell history retains. Most shells record everything by default. A token typed once is in ~/.bash_history indefinitely.

Treat GH_DEBUG=api output as sensitive. It prints request headers, which include the authorisation header. Never paste it into an issue report without redacting.

gh auth login can configure Git to authenticate through gh, which is what makes git push work without a separate token. The mechanism is a credential helper entry:

Terminal window
git config --get-regexp 'credential.*helper'

Output:

credential.https://github.com.helper !/usr/bin/gh auth git-credential

That line tells Git to ask gh for credentials when talking to github.com. Removing it — or having a different helper take precedence — produces the “gh works but git asks for a password” symptom.

Terminal window
gh auth setup-git
gh auth setup-git --hostname github.company.com

gh auth setup-git (re)configures it, which is the fix when the helper is missing or was overwritten by another tool. It is also what you run if you declined the Git configuration during the initial login and later changed your mind.

Note the interaction with other helpers: if you also have osxkeychain or manager configured, Git consults helpers in order and the first with an answer wins. Two helpers holding different credentials for github.com produces intermittent, confusing authentication failures — worth checking when diagnosing one.

  1. Run gh auth status and read every line — note the storage backend, protocol and scopes.
  2. Run gh auth token and confirm it prints a credential. Do not paste it anywhere.
  3. Unset your stored login in a subshell and authenticate with GH_TOKEN instead, confirming gh repo view still works.
  4. Run gh auth refresh --scopes repo,workflow and compare the scopes line before and after.
  5. If you have a second account, add it and practise gh auth switch.

Step 3 is the pattern every CI job uses, and doing it once locally makes CI authentication unmysterious.

  • gh auth login can configure both gh’s credentials and Git’s, and declining the second causes a common confusion.
  • gh auth status is the first diagnostic; active account and token scopes explain most failures.
  • The active account is global state — check it before anything consequential.
  • GH_TOKEN overrides stored credentials entirely, which is what makes CI work without writing to disk.
  • Credentials go to the OS keyring where available and to a plain file where not.
  • Logout removes a credential locally; only revocation invalidates it.

gh auth status is the first diagnostic for anything authentication-related. Active account and token scopes explain the large majority of confusing failures, and the rate-limit trick — a limit of 60 means unauthenticated — settles the rest in one call.

The two things most often missed: declining Git credential configuration during login, which produces “gh works but git asks for a password”; and an exported GH_TOKEN silently overriding a stored login, so gh auth status reports one account while commands behave as another.

For CI, supply GH_TOKEN from a secret store rather than logging in — nothing is written to disk, which is what you want on a machine you do not control.

gh auth login stores credentials in the OS keyring where one is available and falls back to a plain file where it is not. Both behaviours can be forced.

Terminal window
gh auth login --insecure-storage
gh auth status
✓ Logged in to github.com account octocat (keyring)
✓ Logged in to github.com account other-user (/home/you/.config/gh/hosts.yml)

The parenthesised value is the storage backend, and it is worth reading. keyring means the operating system’s secret store. A path means a plain text file readable by anything running as your user.

--insecure-storage exists for headless Linux servers where no Secret Service daemon is running and the keyring simply is not available. Using it is a deliberate trade, and on a machine you share it is the wrong one — prefer GH_TOKEN supplied per invocation from a secret store, so nothing is persisted at all.

Where a keyring exists but gh fell back to a file anyway, the usual cause is a headless session with no unlocked keyring — a container, an SSH session, or a CI runner. That is worth noticing rather than ignoring: a credential you believed was in a keyring sitting in a world-readable-to-your-user file is the kind of assumption that turns into an incident.

Professional ToolkitThe gh api recipes and the PR triage and release-notes scripts are in the Professional Toolkit.