The first post in this series covers why I wait two weeks before installing any new release. Homebrew has no built-in setting to handle this, so what do we do?

Why Homebrew doesn't have a cooldown setting

This isn't an oversight. Homebrew has turned it down on purpose, more than once. Feature requests for a minimum age on brew install and brew upgrade (#21421) and for a global "minimum-release age" setting (#22659) were both closed as not planned. The reasoning is written up in Homebrew's security and supply chain docs:

A blanket cooldown would trade a small, speculative reduction in supply-chain risk for a real, across-the-board delay in shipping critical fixes.

That is a fair argument for the default. It just doesn't stop me from choosing a different tradeoff on my own machine.

My solution: lag the tap metadata

Brew can only install what its formula and cask definitions describe. Those definitions are git clones on disk. If I keep the clones 14 days behind upstream, everything brew sees (install, upgrade, outdated, deps, brew bundle) is at least 14 days old.

That takes three pieces: stop brew from refreshing itself, a script that moves the clones, and a shell function so brew update uses the script.

1. Create ~/.homebrew/brew.env

HOMEBREW_NO_INSTALL_FROM_API=1
HOMEBREW_NO_AUTO_UPDATE=1
HOMEBREW_NO_INSTALL_UPGRADE=1

HOMEBREW_NO_INSTALL_FROM_API is the important one: it makes brew read the local tap clones instead of the live API, which would ignore the lag.

2. Add ~/bin/brew-update-lagged

It resets every tap to the newest commit older than 14 days, and Homebrew itself to the newest release tag older than 14 days.

#!/bin/zsh
set -euo pipefail

BREW=/opt/homebrew/bin/brew
BREW_REPO=/opt/homebrew
CORE=$BREW_REPO/Library/Taps/homebrew/homebrew-core
days=${BREW_LAG_DAYS:-14}

# Colored box on a terminal, plain text in logs and pipes
boxed() {
  local color=$1 msg=$2
  if [[ -t 2 ]]; then
    local line=${(l:${#msg}+2::─:)}
    print -u2 "\e[${color}m┌${line}┐\n│ ${msg} │\n└${line}┘\e[0m"
  else
    print -u2 -- "$msg"
  fi
}

banner() {
  local when=$(git -C "$CORE" log -1 --format=%ct)
  local age=$(( ($(date +%s) - when) / 86400 ))
  local ver=$(git -C "$BREW_REPO" describe --tags)
  if (( age < days )); then
    boxed 31 "brew: COOLDOWN OFF, formulae are only $age days old (want $days). Run 'brew update' to restore."
  else
    boxed 32 "brew: ${days}-day cooldown, formulae as of $(date -r "$when" +%F) ($age days old), Homebrew $ver"
  fi
}

case ${1:-} in
  --banner) banner; exit 0 ;;
  --days)   days=${2:?--days needs a number} ;;
esac
cutoff=$(( $(date +%s) - days * 86400 ))

repos=("$BREW_REPO")
for tap in $($BREW tap); do repos+=("$($BREW --repository "$tap")"); done

for repo in $repos; do
  if [[ -n $(git -C "$repo" status --porcelain --untracked-files=no) ]]; then
    echo "skip $repo: has local changes"; continue
  fi
  upstream=$(git -C "$repo" rev-parse --abbrev-ref --symbolic-full-name '@{u}')

  if [[ $repo == $BREW_REPO ]]; then
    # Homebrew itself: newest release tag older than the cutoff
    git -C "$repo" fetch --quiet --tags origin
    tag=$(git -C "$repo" for-each-ref --sort=-creatordate \
            --format='%(creatordate:unix) %(refname:short)' 'refs/tags/[0-9]*' |
          awk -v c="$cutoff" '$1 <= c { print $2; exit }') || true
    [[ -n $tag ]] || continue
    target=$(git -C "$repo" rev-parse "$tag^{commit}")
  else
    # Taps: newest mainline commit older than the cutoff
    git -C "$repo" fetch --quiet origin
    target=$(git -C "$repo" rev-list -1 --first-parent --before="@$cutoff" "$upstream")
  fi
  [[ -n $target ]] || continue

  git -C "$repo" reset --quiet --hard "$target"
  echo "$repo -> $(git -C "$repo" log -1 --format=%cs "$target"), $(git -C "$repo" rev-list --count "$target..$upstream") commits held back"
done

banner
$BREW outdated || true

Then make it executable:

chmod +x ~/bin/brew-update-lagged

3. Add a brew function to your shell rc

Put it in whichever rc file your terminal actually loads (~/.bashrc, ~/.zshrc, etc) and source the file again.

brew() {
  case "$1" in
    update|update-reset) shift; "$HOME/bin/brew-update-lagged" "$@" ;;
    install|reinstall|upgrade|outdated|bundle) "$HOME/bin/brew-update-lagged" --banner; command brew "$@" ;;
    *) command brew "$@" ;;
  esac
}

Usage

Nothing changes. You use brew exactly the way you already do:

brew update        # moves the taps to 14 days ago instead of today
brew upgrade
brew install <pkg>

The only difference is a banner before install, upgrade, and outdated, so you know the cooldown is on:

┌─────────────────────────────────────────────────────────────────────────────────┐
│ brew: 14-day cooldown, formulae as of 2026-10-11 (14 days old), Homebrew 7.0.10 │
└─────────────────────────────────────────────────────────────────────────────────┘

If the taps are ever less than 14 days old, it turns red and says COOLDOWN OFF.

For an urgent fix, override the cooldown, upgrade the one package, and go back:

~/bin/brew-update-lagged --days 0   # jump to latest, banner goes red
brew upgrade <pkg>
brew update                         # back to the lagged view

Caveats