GitHub revoked your personal access token? Why, and how to replace it on Mac
Pushes that worked yesterday now fail, a script gets 401 Bad credentials, or GitHub sent an email saying a token was revoked. A revoked token cannot be brought back: GitHub's token expiration and revocation page says you have to create a new one. So the work is finding out why it was revoked, which tells you whether you also have a leak to clean up, and then replacing the old value everywhere your Mac has it saved.
What a revoked token looks like
Over HTTPS, git push, git pull on a private repository and git clone of one all fail like this:
remote: Invalid username or token. Password authentication is not supported for Git operations.
fatal: Authentication failed for 'https://github.com/OWNER/REPO/'
The REST API answers with HTTP 401:
{
"message": "Bad credentials",
"documentation_url": "https://docs.github.com/rest",
"status": "401"
}
Two traps. Cloning or fetching a public repository can keep working with a dead token, so a broken token often shows up first on the push. And the same error appears for a token that was never valid, such as a typo or your account password, which GitHub stopped accepting for Git operations.
Why GitHub revoked it
GitHub's revocation page lists these causes for personal access tokens:
- It expired. On its expiration date a token is revoked automatically. This is the common, harmless case.
- It was pushed to a public repository or a public gist. A valid personal access token, OAuth token or GitHub App token found there is revoked automatically. If this is the reason, the token leaked: read the next section.
- It went unused for a year. GitHub revokes an OAuth token or personal access token that has not been used in one year.
- Someone reported it. Anyone can submit a token they found to GitHub's credential revocation API, without logging in. GitHub's announcement says a valid token is revoked, the owner gets an email at their primary address, and an
oauth_access.revokeevent is logged. A token found in a public place by a stranger is a leak too. - You, an app or an owner revoked it. Revoking an app's authorization revokes its tokens. Organization owners, and enterprise owners on GitHub Enterprise Cloud, can revoke SSO authorizations and delete credentials, for example after a security incident; GitHub says you get an email when they do.
- Too many OAuth tokens. GitHub issues at most ten tokens per user, application and scope combination, and going past that revokes an older one. This is about OAuth tokens that apps get when you authorize them, not tokens you create by hand.
To see which happened, open GitHub, click your profile picture, Settings, then Security log under "Archives" (reviewing your security log). The revocation page says an expired or revoked token may show up there as oauth_authorization.destroy. Also check the token list itself: Settings, Developer settings, Personal access tokens, then Fine-grained tokens or Tokens (classic), and look for the token's name and expiration.
If it was pushed or posted publicly
The revocation already killed the token, which is the urgent step done for you. Three things are still yours:
- Look for use. Go through the security log, and the organization's audit log if the token reached an organization, for the time between the push and the revocation. Anything created with the token, such as a new SSH key, deploy key or webhook, survives the revocation and has to be removed by hand.
- Check the rest of the commit. GitHub revokes its own tokens. An AWS key, Stripe key or database password in the same file is a separate problem; see I leaked my API key, what do I do?
- Take it out of the code. Move the value into an environment variable or a secrets store, then follow how to remove an API key from git history. Turn on push protection so the next one is refused before it reaches GitHub.
Make the replacement
In Settings, Developer settings, Personal access tokens, create a fine-grained or classic token with only the repositories and permissions the job needs, and an expiration date. Copy the value from the page GitHub shows after you generate it, straight to where it is going. The prefix tells you what you have: ghp_ is a classic token and github_pat_ a fine-grained one (GitHub's token formats).
Replace it in the Keychain, for git over HTTPS
First find out where git keeps the token. On a Mac using Apple's git, this usually prints osxkeychain, set in the system gitconfig that ships with Apple's developer tools:
git config --show-origin --get-all credential.helper
osxkeychain keeps the token as an "Internet password" item for github.com in your login keychain. When the server rejects a saved credential, git asks the helper to erase it (the reject step described in git credential), so the next push often prompts on its own. If it keeps failing without asking, erase the item yourself, as in GitHub's updating credentials from the macOS Keychain:
git credential-osxkeychain erase
host=github.com
protocol=https
Type the two lines, then press Return on an empty line. Or open Keychain Access, search for github.com and delete or edit the "Internet password" item. To check whether an item exists without showing the password (that needs -g or -w):
security find-internet-password -s github.com
On the next push, enter your GitHub username, and paste the new token when git asks for a password.
store means the token sits unencrypted in ~/.git-credentials (git-credential-store). Edit the github.com line, or delete the line and push again. A token inside the remote URL, such as https://ghp_…@github.com/OWNER/REPO, is sent on every push whatever the helper holds. Check with git remote -v and fix it with git remote set-url origin https://github.com/OWNER/REPO.git.
Replace it for the GitHub CLI
gh auth status tests each saved account and exits with 1 when one has a problem. Run gh auth login to sign in again. Then gh auth setup-git makes git use gh as its credential helper, so git and gh share one login.
If gh keeps failing after a fresh login, look for an old token in your environment. GH_TOKEN and GITHUB_TOKEN take precedence over stored credentials (gh help environment). This shows which are set, without printing the values:
env | grep -E '^(GH|GITHUB)_TOKEN=' | cut -d= -f1
Find the other copies of the old token
A personal access token tends to get pasted into more places than anyone remembers: a shell profile, a project .env file, ~/.netrc, an .npmrc for GitHub Packages, a CI secret. Each one fails separately until you replace it. To list files that contain anything shaped like a GitHub token, with names only and no values printed:
grep -IlE 'gh[pousr]_[A-Za-z0-9]{36}|github_pat_[A-Za-z0-9_]{82}' \
~/.zshrc ~/.zprofile ~/.bashrc ~/.bash_profile ~/.netrc ~/.git-credentials ~/.npmrc 2>/dev/null
grep -rIlE --exclude-dir=node_modules --exclude-dir=.git \
'gh[pousr]_[A-Za-z0-9]{36}|github_pat_[A-Za-z0-9_]{82}' ~/Projects
Change ~/Projects to wherever your code lives. Remove each match, or put the new token in it, and open a new terminal so the shell reloads the profile. If you ever typed export GITHUB_TOKEN=…, it is also in your shell history: see the guides for .zsh_history, .bash_history and fish history, and how to keep secrets out of shell history for next time. CI systems and other machines need the new value too; nothing on your Mac can update those.