A private tap kept breaking my `brew upgrade`
A scoped shell wrapper gives one private formula the GitHub credential it expects without exporting that token across the whole shell session.
My normal brew upgrade kept failing on one formula from a private tap:
Error: Failed to download resource "private-cli (1.38.0)"
Download failed: https://github.com/your-org/private-cli/releases/download/v1.38.0/private-cli-1.38.0.tar.gz
curl: (22) The requested URL returned error: 404
The public packages were fine. The private formula could not download its release, so that one failure stopped the complete upgrade.
The private formula expected an environment variable
I was already logged in with GitHub CLI, and that account could read the private repository. In Homebrew 7.0.2, the GitHub API client can call gh auth token. Homebrew's generic formula download path does not automatically attach that credential. This formula used custom download code and explicitly read HOMEBREW_GITHUB_API_TOKEN.
Without the variable, the formula made an unauthenticated request. GitHub can return 404 Not Found for a private resource, making an authentication failure look like a missing release. A 404 alone does not prove that diagnosis because a wrong URL or missing asset produces the same response.
The bad fix: export the token globally
The obvious fix is to put this in .zshrc:
export HOMEBREW_GITHUB_API_TOKEN="$(gh auth token --hostname github.com)"
It fixes the download, but it is still the wrong default. The configuration file does not contain the literal token, yet it asks gh to copy the credential into the shell environment whenever an interactive Zsh session starts.
The problem starts when the shell evaluates the line. The token becomes part of the shell environment. By default, every process launched from that shell inherits it while the variable remains exported, and a descendant can retain its copy after the shell exits. An editor, build tool, package manager, dependency install script, or any other process launched from the terminal can read it. Most of those processes have no reason to receive a GitHub credential.
The token returned by a normal GitHub CLI login may also have much broader repository access than this one release download needs. Exporting it globally turns that credential into ambient authority for the complete shell session.
Fish makes this approach even easier to get wrong because it reads config.fish for interactive and non-interactive shells. An interactive guard reduces that exposure, but every child of the interactive shell still inherits the token. I do not recommend exporting HOMEBREW_GITHUB_API_TOKEN from either shell's startup file.
The fix: scope the token to Homebrew
I prefer wrapping brew. The command stays the same, but only that Homebrew invocation and the processes it starts receive the token as an environment variable.
Fish: ~/.config/fish/config.fish
function brew
set -l token (gh auth token --hostname github.com); or return
test -n "$token"; or return 1
HOMEBREW_GITHUB_API_TOKEN="$token" command brew $argv
end
Zsh: ~/.zshrc
brew() {
local token
token="$(gh auth token --hostname github.com)" || return
[[ -n "$token" ]] || return 1
HOMEBREW_GITHUB_API_TOKEN="$token" command brew "$@"
}
I still run the normal command:
brew upgrade
The guards stop before running Homebrew if gh cannot return a token. command brew calls the executable instead of recursing into the wrapper function.
This wrapper supplies the token to every brew command, not only upgrades. It also performs a credential lookup for every call. That is still a much smaller boundary than every process started during the lifetime of my shell.
The token still crosses one trust boundary
The wrapper cannot hide the token from Homebrew or formula code involved in that run. The private formula needs the credential to authenticate, so I only use this with taps and formulae I trust.
A repository-scoped, read-only fine-grained token can reduce the impact of a leak when the formula uses GitHub's supported release-asset API. Compatibility still depends on the formula's download implementation.
None of these snippets enables automatic upgrades. They only decide when the existing credential enters the process environment when I run Homebrew myself.