>
Developer

Stop Typing Passwords Into GitHub Forever

Stop Typing Passwords Into GitHub Forever

I had a small heart attack last month when a coworker screenshared a terminal session and a personal access token scrolled past in plaintext. The token had read access to a private repo with customer data. It was scoped correctly. It was also sitting in the scrollback buffer for anyone on the call to see, and in the shell history file for anyone with read access to my home directory to grep. I rotated it that afternoon, then spent the next two weeks moving every token, key, and credential I touch out of the prompt entirely.

GitHub has supported keyless authentication for a while now, but the docs are scattered across three different blog posts, a security hardening guide, and a CLI help page that buries the lede. The result is that most developers still paste a personal access token (PAT) into their terminal the first time they clone a private repo, then never think about it again. That token sits in plain text in ~/.gitconfig or a keychain entry that may or may not be encrypted, and it has whatever scopes you granted it six months ago when you were in a hurry.

The fix is not complicated. It just takes an afternoon.

What “keyless” actually means on GitHub

The GitHub flow uses the same web-based auth you already use to log into github.com, plus a short-lived token that the CLI fetches behind the scenes. When you run a git command against a GitHub repo, the CLI opens a browser window, you click “authorize,” and the CLI gets a token that expires in about an hour. After the hour, the next command opens the browser again. There is no long-lived secret sitting on disk.

The protocol behind this is the same one used by every other major code host. GitHub calls its implementation the GitHub CLI. GitLab has the same flow under a different name. Bitbucket, Azure DevOps, and the smaller hosts all support some version of it. The point is that your laptop never holds a credential that is valid for more than an hour, which means a stolen laptop backup or a leaked dotfiles repo does not hand an attacker the keys to your account.

First requirement is the GitHub CLI (gh) installed. It is a small binary, available through Homebrew, apt, and the Windows package manager. Second requirement is that the auth flow only kicks in for HTTPS remotes. If you are still on SSH, you have a different (also good) set of options, but the keyless flow described here is HTTPS-specific.

What I had to change

The actual migration took about 90 minutes for the half-dozen machines and CI runners I touch. The shape of the change:

  • Install the GitHub CLI. On macOS, brew install gh. On Ubuntu, the official GitHub apt repo (a maintained package source that ships the latest stable version) is the cleanest path. On Windows, the MSI installer. The CLI is signed and the binary is small.
  • Authenticate once per machine. Run gh auth login, choose “GitHub.com,” choose “HTTPS,” choose “Login with a web browser.” The CLI prints a one-time code, opens your browser, you paste the code, click “authorize,” and you are done. The token gets stored in the OS keychain, encrypted at rest.
  • Switch git remotes from any PAT-based URL to the standard HTTPS form. git remote set-url origin https://github.com/{owner}/{repo}.git. The CLI handles the auth handshake from there. You do not need to embed credentials in the URL.
  • Remove old PATs from your git config. If you have a [credential] or [url] block in ~/.gitconfig with a token in it, kill it. Same for any .git-credentials file in your home directory. Same for any environment variable like GITHUB_TOKEN that is set in your shell rcfile.
  • Rotate the old PATs. Even after you stop using them, treat them as compromised. Go to github.com → Settings → Developer settings → Personal access tokens → Tokens (classic) and “Revoke” each one. Generate a fresh one only if you have a script or CI job that genuinely needs long-lived credentials.

The CI angle is where most people stall. If your CI runs git checkout against private repos, it has been using a PAT stored as a secret. That is fine, but consider switching to the GitHub Actions OIDC token (an automatically-issued, short-lived identity token that lets your workflow authenticate to GitHub without a stored secret). The setup is a one-time per-repo config change and the workflow file change is two lines.

What goes wrong if you skip steps

The most common failure mode I have seen is half-migrating. A developer installs the CLI, authenticates once, and assumes the old PAT is dead. The PAT is not dead. It is still in .git-credentials, still in ~/.gitconfig, and still valid until its expiration date. If the developer later opens a fresh terminal session on a server that does not have the CLI installed, git falls back to the stored credential and uses the PAT silently. The keyless flow is opt-in for each new environment.

Another failure mode is leaving the PAT with overly broad scopes. A lot of people generate a PAT with every scope checked because the GitHub UI is easier to use that way. A token with repo, workflow, admin:org, and delete_repo is a small breach waiting to happen. The principle of least privilege says you should generate a token with the smallest set of scopes the workflow needs, and use a different token for each workflow. If a token leaks, the blast radius is one workflow, not the whole account.

A third failure mode, rarer but worse, is checking the PAT into a repo. Every major code host runs secret scanning on push (a service that automatically greps new commits for tokens and pings the provider to revoke them), and GitHub’s scanner is good. But it is not instant, and a leaked PAT can be harvested by an automated crawler within minutes. The point of the keyless flow is that there is no PAT to leak.

What this did not fix

I want to be candid about what the keyless flow does not solve. It does not help if your laptop itself is compromised. A keystroke logger sees your one-time code in the browser window, and an attacker who gets the code inside the auth window can finish the flow and get a token. The one-time code is short-lived (about 10 minutes), so the window is narrow, but it is not zero.

It also does not eliminate the need for some form of long-lived credential in CI. A self-hosted CI runner that checks out private repos needs something to authenticate with. The answer there is either an OIDC token (the cleanest option, if your host supports it) or a tightly scoped PAT with a 30-day expiration. The PAT in CI is a smaller risk surface than the PAT on a developer’s laptop, because the CI environment is more controlled, but it is not zero risk.

Finally, it does not help with anything outside GitHub. Your cloud provider keys, your package registry tokens, your database passwords, your SSH keys to production servers, all of those are separate problems with separate fixes. The keyless GitHub flow is one slice. It is an important slice because GitHub is the system that holds your source code, but it is one slice.

What I would tell past me

Three things, in priority order:

  • Install the GitHub CLI on every machine you develop on, including servers you ssh into. The install is a one-liner. The auth is one command. The upside is that there is no long-lived secret on the disk for an attacker to find.
  • Rotate every PAT you have ever generated. Treat the old ones as compromised even if you never posted them anywhere. The cost is ten minutes of clicking through the GitHub settings page. The upside is that a stale token with admin:org scope is not sitting in a backup from 2024.
  • Audit your CI configs. If any of them use a PAT stored as a secret, switch to OIDC where possible. The setup is annoying the first time and trivial every subsequent time.

Trade-offs

The keyless flow has one real downside. It requires a browser to be available on the machine where you are running git. On a server you ssh into, that means an X11-forwarded browser, a separate machine you copy-paste a one-time code into, or a configuration where the CLI talks to the GitHub API directly with a preconfigured token. The preconfigured token option is closer to the old PAT workflow, but the token is stored in the OS keychain rather than a plaintext file, and you can scope it per-machine. On a developer laptop with a browser, this is not a real cost.

The other downside is the operational one. When something breaks and you need to debug, the keyless flow adds a hop (CLI talks to GitHub, GitHub talks to your browser, browser talks back to CLI) that is harder to reason about than “the PAT in my .gitconfig is wrong.” If you have ever debugged auth issues at 2 AM, you know that simpler is better. The keyless flow is more moving parts.

For most individual developers, the keyless flow is a clear win. For teams with strict change-management policies, the migration takes longer because every developer’s machine and every CI runner needs the change, and there is a window where some endpoints use the new flow and others use the old. For small teams, the migration is an afternoon. For large teams, plan for a quarter.

If you have ever pasted a GitHub PAT into a terminal, the answer is to spend the 90 minutes this weekend and stop. The token in your gitconfig is a small bomb, and the keyless flow is a real, supported, audited way to defuse it.

Leave a comment