Back to Explore
NewsVibeFix 编辑部Updated Nov 2, 2026

A Leaked Token That Dies in Hours: GitHub CLI's Expiring OAuth Preview Is Worth Piloting Now

On 21 October 2026 GitHub CLI v2.103.0 previewed expiring OAuth credentials: access tokens that die within hours and refresh tokens that rotate automatically. This piece covers the two commands, the cached-token scripts that will break, the git 2.46 and GHES 3.23 floors, and why the pilot belongs before the default flips.

Real photograph of hands typing code on a laptop with colorful code on screen in a dark setting; not an official GitHub screenshot

The short version: a leaked gh token will soon die within hours

Real photograph of hands typing code on a laptop with colorful code on screen in a dark setting; not an official GitHub screenshot

On 21 October 2026, GitHub announced in its official changelog that GitHub CLI (gh), starting with v2.103.0, offers expiring OAuth credentials as a public preview. Opt in, and gh no longer stores a single never-expiring access token. Instead it stores a pair: an access token that expires within hours, plus a rotating refresh token. gh refreshes the token automatically whenever it is needed, so you stay signed in without reauthenticating — and if an access token leaks, it becomes unusable within hours instead of lasting indefinitely until someone notices and revokes it by hand.

The qualifiers in that sentence matter, because they set the true weight of the news. First, this is a public preview, not a default. Second, it is opt-in: choose nothing and nothing changes. Third, GitHub says plainly that it plans to make expiring credentials the default for new sign-ins in a future release. So this is not a forced migration. It is advance notice. People who pilot now are doing the same work that everyone will have to do later — checking their scripts and pipelines — except they are doing it before the default flips, rather than on the day it flips.

My judgment: this is the most consequential command-line security change of the year for ordinary developers, not because the technology is exotic, but because it fixes the kind of risk people most easily ignore — an old credential sitting on some machine, in some script, in some backup. Credential leaks are not rare. The real difference is how much time you have after one happens. Shrinking “indefinitely” to “a few hours” changes the arithmetic for both sides. It is not bulletproof, but it narrows the worst case dramatically.

What actually changed: two commands and one setting

The usage itself is deliberately simple, which is part of why it is worth piloting. For a single sign-in, pass the flag: gh auth login --expiring. To prefer expiring credentials for every future sign-in, set it once: gh config set expiring_credentials enabled. After that there is nothing new to run day to day; gh refreshes the credential for you in the background. To check the state of an expiring credential, run gh auth status, which shows whether the credential is refreshable and when the current access token expires. The v2.103.0 release notes add that the status display includes a relative time-to-expiry — “expires in a few hours” rather than a timestamp you have to decode yourself.

The other side of opt-in is equally important: not opting in genuinely changes nothing. GitHub's changelog is explicit — if you do not opt in through the flag or the configuration setting, existing sign-ins and manually supplied tokens keep working exactly as before. That means a pilot can start on one machine without persuading the whole team or rebuilding a pipeline first. Run expiring credentials on your own laptop for a week of ordinary commands, confirm nothing breaks, and only then decide whether to extend it to shared machines and automation accounts.

One more detail deserves to be stated on its own: expiring credentials are requested from the server, not manufactured locally. gh asks for refreshable credentials, and if a server does not support them, gh falls back cleanly to a normal non-expiring token instead of failing. That fallback matters most for self-hosted users, and I will come back to it in the section on version floors. But it also means --expiring is a preference, not a guarantee. What credential you actually end up with depends on what the server supports, and any rollout plan should remember that rather than assuming “flag on” always means “token expires.”

Why now: permanent tokens meet the agent era, and the cost of a leak changed

A gh OAuth token with no expiry was a tolerable compromise back when only a person sitting at a keyboard ran gh commands. If the token leaked, the person revoked it, signed in again, and moved on. But today gh is called by much more than people. Coding agents run gh in the background to check issues, open pull requests and push branches. Continuous integration runs on shared runners. A team laptop may be running several automation tasks at once. The more callers there are, the more paths exist for a token to be copied, cached, logged or backed up — and “until someone notices” is precisely the condition that automation environments are worst at satisfying, because nobody watches one machine's credentials every day.

The storage story makes it worse. On machines without a system credential store, gh falls back to writing the token to a plain-text file. That is not unique to gh — many command-line tools have a similar fallback — but it does mean that one infostealer harvest, one careless dotfiles backup, or one home directory archived and sent to someone else can carry a permanently valid GitHub identity along with it. Expiring credentials do not prevent a token from being stolen, and that should be said honestly. What they change is the story after the theft: what was stolen is an access token that dies on its own within hours, not a long-term pass.

For completeness, half a caveat belongs here too: the refresh token is still a key. The design moves risk from “one secret that never expires” to “one secret that rotates,” it does not make secrets disappear. Rotation means an old refresh token stops working once it has been rotated, which is much stronger than a static secret — but it still requires you to protect the machine and the credential store where the tokens live. Reading expiring credentials as “credential hygiene is now unnecessary” gets the news backwards. The correct reading is “credential hygiene now has a larger margin for error,” not “credential hygiene is gone.”

How the two keys work together: the short-lived one works, the rotating one renews

A simple analogy: the access token is a day pass, and the refresh token is the renewal credential kept in a safe. gh does its daily work with the day pass; when the day pass expires, gh renews it itself without sending you back to the front desk. Two points in that analogy are worth noting. First, renewal is automatic, and that is the fundamental difference from any scheme that asks you to reauthenticate every few hours — a scheme like that would never survive real daily work. Second, the renewal credential rotates: once it has been used, the previous one stops working, so even if a refresh token leaks, the window in which it can be used is compressed.

There is a secondary benefit too: credential state becomes something you can inspect. A permanent token had no interesting state — it worked until it was revoked. Expiring credentials introduce two observable facts, “when does it expire” and “can it be refreshed,” and gh auth status puts them on display. For anyone managing multiple machines, that is a new debugging handle: when gh suddenly stops working on one machine, the status output tells you whether the problem is “expired and unrefreshable” or “never on expiring credentials at all,” and the investigation goes in a different direction from the start.

But do not overstate automatic refresh. Refresh happens when gh itself makes a call. It solves the problem of “the credential gh uses,” not the problem of “every program that ever received a copy of this token.” That distinction leads directly to the most important section of this piece: what breaks when you switch.

What breaks first: anything that caches a token and reuses it later

GitHub's changelog devotes a warning to this, and the v2.103.0 release notes mark the same point as important: expiring access tokens should not be cached. Any script or extension that runs gh auth token and stores the output for later reuse will eventually be left holding an expired token. The fix is mechanical, not clever: change “fetch once, store, reuse everywhere” to “fetch fresh each time you need it.” Each call to gh auth token refreshes when needed, so a fresh read always returns a currently valid token. The hard part is finding the scripts. Wrappers, upload scripts, chat-ops bots, cron jobs, forgotten personal utilities — all of them are hiding places. Grepping for them before the pilot is far cheaper than firefighting after it.

Two categories of tokens are explicitly unaffected, and that is reassuring for a pilot. First, manually supplied tokens: a classic personal access token handed in with gh auth login --with-token keeps working exactly as before, according to the changelog. Second, environment-variable tokens: the v2.103.0 release notes state plainly that tokens supplied through GH_TOKEN or GITHUB_TOKEN are never refreshed. That means the CI path that authenticates through environment variables will not suddenly change because you enabled expiring credentials on a development machine. The flip side is equally true: that path's security model does not automatically improve with this update. Its risk window is still as long as the token's lifetime, and it still needs its own rotation discipline.

A practical pilot order follows from this: start with expiring credentials on a personal machine and run ordinary commands; then audit scripts and extensions, converting cached reads to fresh reads and setting aside any workflow that genuinely depends on a long-lived token; only then consider extending the setting to shared environments. GitHub's own advice is to hold off on opting in for workflows that depend on a token staying valid for a long time. The point of a preview is that it permits this kind of staged rollout instead of forcing everyone to switch on the same day.

Two floors to clear first: git and the self-hosted server

If git authenticates over HTTPS through gh, there are two companion steps. First, make gh your git credential helper with gh auth setup-git, so that when git needs a credential it asks gh, which serves and refreshes it, and HTTPS clones and pushes keep working without you thinking about expiry. Second, upgrade git to 2.46 or newer. That version lets gh mark expiring credentials so they are not cached past their lifetime; the changelog says older git still works but prints a one-time recommendation to upgrade. That sounds like a suggestion, but the underlying risk is real: an older caching helper can hand out an expired token after expiry, quietly defeating the point of expiry on the git path. Treat this as a requirement, not an option — especially on machines where agents clone repositories frequently.

Self-hosted users should look at GitHub Enterprise Server 3.23. Expiring tokens are available on github.com and ghe.com today; self-hosted servers issue them starting with GHES 3.23, and on earlier versions opting in falls back cleanly to today's non-expiring behavior without erroring and without pretending to be expiring. That clean fallback deserves credit, because it avoids the worst failure mode: believing you are protected when you are not. But it also creates a management requirement. In a mixed environment, you have to know which servers are already on 3.23 and which are still in fallback mode, because the same configuration will deliver different real security on different servers.

Taken together, the conclusion is simple: expiring credentials are not a “flip a switch and done” feature. They are a small authentication-hygiene cleanup. Upgrade git, set the credential helper, confirm the server version — do those three things and the benefit arrives intact; skip them and the benefit leaks away on one specific path or another.

My judgment: pilot now, because the default flip will not wait for you

GitHub has already said it: expiring credentials are opt-in today and planned to become the default for new sign-ins in a future release. The subtext is that the preview period is your migration window. Doing the pilot during the window costs one machine and one script audit. Doing the same work after the default flips costs the same work plus every un-audited script failing at once. Same job, very different price. That is the entire argument for piloting now; it needs no grander justification.

But a pilot should be done in the right order. My suggestion is three steps. First, enable expiring credentials on a personal machine, run it for a week, and confirm ordinary commands, git operations and common extensions show no anomalies. Second, audit scripts and extensions, convert cached token reads to fresh reads, and list separately any workflow that must be left on a long-lived token for now. Third, set a team-wide rollout date, and write the git 2.46 and GHES 3.23 version conditions into the rollout checklist. After those three steps, the default flip becomes an unremarkable settings change rather than an emergency debugging session.

Finally, back to the value itself. Expiring credentials will not eliminate credential leaks, and they do not replace rotation, least privilege or auditing. Their value is in shrinking the consequence of a leak from “indefinite” to “a few hours,” and in doing so without interrupting daily work through automatic refresh. Many security improvements are like this: they do not solve everything, but they pull the ceiling of the worst case down a long way. For anyone who uses gh every day, that kind of improvement is worth taking seriously during the preview — not on the day it becomes the default, when you discover that an old habit of caching a token is still hiding in your own scripts.

One more thing: the preview's real value is the checklist it exposes

Teams evaluating a preview often ask “is it stable?” before “should we adopt it?” For expiring credentials, the order should arguably be reversed: use it to illuminate your own current state first, then set the rollout pace. Once you enable expiring credentials on a pilot machine, problems that were previously hidden tend to surface on their own — a cron job nobody has touched in months suddenly errors, an extension retries with a stale token after expiry, a shared machine's git prints an upgrade notice because it is too old. Those errors look like trouble, but they are really a free inventory: those problems existed before, they were just masked by permanent tokens.

Seen that way, GitHub's three-part design — opt-in, clean fallback, default later — is a restrained way to ship a security change. It forces nobody to switch on the same day, and it does not let unsupported servers pretend to be supported. The matching response from users should be equally restrained: verify on one machine, audit the scripts, then set a rollout date. The thing security changes fear most is not slowness but bigness: a single big-bang switch whose debugging costs more trust than the risk it was meant to fix. The preview period exists precisely so you do not have to pay that price.

Views 1Comments 0

Comments (0)

ME
0/1000
Loading comments...

Sources

Browse projectsPublish your project

Related articles

A real photograph of a multi-monitor developer desk with terminals and code on dark screens; not a Herdr official image
News
Close the Window, Keep the Agents: Herdr's Desktop App and herdr-core

On 29 October 2026 Herdr shipped a native desktop app on a new runtime, herdr-core: terminals and agents live in the core, not the window. This piece separates the three pieces (TUI, app, core), explains the file-based interface, the hc commands, the exact crash boundary, and where it sits between tmux and graphical agent platforms.

Developer WorkflowProduct LaunchAI Coding
A real photograph of a development team gathered around screens showing code in an office; not a Microsoft or GitHub official image
News
From Preview to Generally Available: GitHub Moves Into the Teams Chat Window

On 30 October 2026 Microsoft made the GitHub app in Teams generally available, merging notifications and Copilot coding into one app. This piece separates the GA from the 23 October merge announcement, walks through Canvas, multi-repo and MCP support, the admin checklist, and why the whole thread becoming context is the part to get right before rollout.

Product LaunchTeam CollaborationAI Coding