What the auth gate actually blocked
gh runs one shared check before most subcommands execute: is there a token, or a logged-in host, available. Individual commands don’t get their own custom login logic; they either run behind that shared gate or they’re explicitly marked to skip it. Before v2.96.0, gh release download wasn’t marked to skip it, so a CI step pulling a release asset from a completely public repo still hit the same “please authenticate” wall as any command that genuinely needed write access.
gh extension install got the same exemption three months earlier, in v2.90.0. The problem there was more specific: Codespaces issues SAML-scoped tokens that fail authorization checks against extension repos they were never scoped for, so installing a public extension from inside a Codespace could fail even though installing it never needed elevated access. PR #13176 fixed it with a one-line opt-out, cmdutil.DisableAuthCheck, added specifically to the extension install subcommand.
PR #13723, which shipped the gh release download change in v2.96.0, points at that exact precedent:
“Release endpoints for public repositories are readable anonymously over REST, so the login gate is unnecessary. Removing it is the same one-line
cmdutil.DisableAuthCheckchange that #13176 made forgh extension install.”
Both fixes trace back to the same tracking issue, #2680, “Allow certain requests to be unauthenticated” - a running list of gh commands that were gated by default even when the underlying GitHub API endpoint never required a session in the first place.
Structural Comparison Matrix
| Operational Aspect | Before v2.96.0 | v2.96.0+ |
|---|---|---|
| Public repo, no token present | Fails at the shared auth gate before the download starts | Succeeds; the REST release endpoint is read anonymously |
| Private repo, no token present | Fails at the shared auth gate | Fails differently: “release not found,” the same response as a nonexistent repo |
| Token present | Used for the request | Still honored opportunistically; nothing about supplying one changed |
Parity with gh extension install | None; that command got its own-command exemption in v2.90.0 (April 2026) | Matched; both now carry the same cmdutil.DisableAuthCheck opt-out |
| CI wiring for a public-asset step | A GH_TOKEN env line was required just to clear the gate | Optional; only needed for private repos or to get the higher rate limit |
What actually happens now, verified live
How this was tested
Since GitHub CLI v2.96.0 has already shipped, showing the old behavior meant
pointing gh at an empty, isolated config directory with no GH_TOKEN or
GITHUB_TOKEN set, so it had zero stored credentials to fall back on. gh auth status confirmed that isolated session was genuinely unauthenticated
before either command below ran against it.
With that isolated, credential-free config, gh release download against a real public release succeeds with no token at all:
$ gh release download v2.96.0 --repo cli/cli -p "gh_2.96.0_checksums.txt"
$ ls
gh_2.96.0_checksums.txtNo prompt, no error, exit code 0. That’s gh’s own v2.96.0 checksums file, pulled from cli/cli’s public release, using the exact repo the release notes’ own example points at.
The shared auth gate is still very much alive elsewhere in the same binary. gh release view never got the same opt-out gh release download did, so running it against the same isolated, unauthenticated session reproduces the exact wall gh release download itself used to hit before v2.96.0:
$ gh release view v2.96.0 --repo cli/cli --json assets
To get started with GitHub CLI, please run: gh auth login
Alternatively, populate the GH_TOKEN environment variable
with a GitHub API authentication token.Exit code 4. That message comes from the same shared pre-command check gh release download was routed through before v2.96.0; only the per-command opt-out changed, not the message itself.
Use it in CI: drop the token for public assets
gh ships preinstalled on GitHub-hosted runners, so a workflow step pulling a public release asset no longer needs a token wired in just to clear the auth gate:
name: Fetch a public release asset
on:
workflow_dispatch:
jobs:
download:
runs-on: ubuntu-latest
steps:
- name: Download checksums from a public release
run: >
gh release download v2.96.0 --repo cli/cli
-p "gh_2.96.0_checksums.txt" --clobberNo env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} line, and no PAT stored as a repo secret for a step that never needed write access in the first place. --clobber overwrites a stale file from a previous run instead of failing; --skip-existing does the opposite if the goal is to avoid re-downloading unchanged assets, and -D <directory> picks a target other than the current directory, per gh’s own manual for gh release download.
One caveat worth keeping in mind: dropping the token doesn’t remove every reason to keep one. Unauthenticated requests share GitHub’s public rate limit of 60 requests per hour, against 5,000 per hour for an authenticated request. A workflow that only downloads one asset from one public repo won’t notice. A workflow that also calls the API repeatedly in the same run, across matrix jobs sharing the same runner IP, might still want the token, for the rate limit headroom, not because the download itself demands it anymore.
What else shipped in the same release
v2.96.0 wasn’t a single-change release. The same version fixed a real remote-code-execution path in gh codespace jupyter, where a malicious Codespace could hand the CLI a vscode:// URL and get command execution on the victim’s machine after one click-through. That fix is unrelated to the auth-gate change here and covered on its own in Update gh CLI Now: Codespace Jupyter RCE Fixed.
Both changes, along with the rest of gh’s 2026 expansion into agent-skill management and Discussions support, are covered in GitHub CLI’s 2026 agent-era expansion, the hub this post is part of.
Drop the token from any CI step that only downloads a public release asset; it’s dead weight now, not a requirement. Keep it anywhere the repo is private, or where the same job leans on the API enough that the rate-limit gap actually matters.
Browse more coverage like this in the Dev Tools archive.







