Skip to content

Instantly share code, notes, and snippets.

@whomwah
Last active March 12, 2026 11:03
Show Gist options
  • Select an option

  • Save whomwah/6c303d8a139db782b89ea5eeb6234818 to your computer and use it in GitHub Desktop.

Select an option

Save whomwah/6c303d8a139db782b89ea5eeb6234818 to your computer and use it in GitHub Desktop.
Upgrade only patch-level Homebrew packages (e.g. 1.2.3 → 1.2.4). Scans brew outdated for packages where only the patch version changed, then offers to upgrade them — so you can apply low-risk security/bug-fix updates without pulling in breaking minor or major changes.
#!/bin/bash
#
# brew-security.sh — Upgrade only patch-level Homebrew packages
#
# Scans `brew outdated` for packages where only the patch version has changed
# (e.g. 0.9.8 -> 0.9.9) and offers to upgrade them. Skips minor/major bumps
# so you can apply low-risk security and bug-fix updates without accidentally
# pulling in breaking changes.
#
# Before each upgrade, performs a dry run to detect dependent packages that
# would also be upgraded. If any dependent would receive a non-patch (minor or
# major) version bump, the user is warned and asked to confirm before that
# package is upgraded.
#
# Usage: brew-security.sh [-h]
#
# Options:
# -h Show this help message and exit
# --- Help -------------------------------------------------------------------
if [[ "$1" == "-h" ]]; then
sed -n '2,/^$/p' "$0" | sed 's/^# \{0,1\}//'
exit 0
fi
# --- Fetch outdated packages ------------------------------------------------
echo "Checking for Homebrew patch updates (e.g., 0.9.8 -> 0.9.9)..."
# --verbose includes version details: "zoxide (0.9.8) < 0.9.9"
# Without --verbose, brew outdated only prints package names.
outdated_info=$(brew outdated --verbose)
if [ -z "$outdated_info" ]; then
echo "No outdated packages found."
exit 0
fi
# --- Parse and filter -------------------------------------------------------
patch_list=()
while read -r line; do
# Expected line format: name (installed_ver) < available_ver
# Packages with multiple installed versions look like:
# sqlite (3.51.0, 3.51.1, 3.51.2) < 3.51.2_1
name=$(echo "$line" | awk '{print $1}')
# Extract all versions from inside the parentheses, then take the last
# one — that's the most recent installed version.
old_ver=$(echo "$line" | grep -oE '\([^)]+\)' | tr -d '()' | awk -F', ' '{print $NF}' | tr -d ' ')
# The available (new) version is always the last field on the line.
new_ver=$(echo "$line" | awk '{print $NF}')
# A patch update has the same MAJOR.MINOR but a different full version.
old_major_minor=$(echo "$old_ver" | cut -d. -f1,2)
new_major_minor=$(echo "$new_ver" | cut -d. -f1,2)
if [[ "$old_major_minor" == "$new_major_minor" && "$old_ver" != "$new_ver" ]]; then
echo " Found patch: $name ($old_ver -> $new_ver)"
patch_list+=("$name")
fi
done <<< "$outdated_info"
# --- Results ----------------------------------------------------------------
if [ ${#patch_list[@]} -eq 0 ]; then
echo "No patch-level updates available."
exit 0
fi
echo ""
echo "The following packages have patch updates available:"
for pkg in "${patch_list[@]}"; do
echo " - $pkg"
done
echo ""
read -p "Would you like to upgrade these ${#patch_list[@]} packages? (y/N): " confirm
if [[ ! "$confirm" =~ ^[Yy]$ ]]; then
echo "Upgrade skipped."
exit 0
fi
# --- Upgrade with dependent check -------------------------------------------
for pkg in "${patch_list[@]}"; do
echo ""
echo "Checking $pkg..."
# Dry run to discover everything that would actually be upgraded,
# including any dependent packages brew would pull along.
dry_run=$(brew upgrade --dry-run "$pkg" 2>&1)
# Parse lines of the form: "name old_ver -> new_ver"
patch_deps=()
non_patch_deps=()
while read -r dep_line; do
dep_name=$(echo "$dep_line" | awk '{print $1}')
dep_old=$(echo "$dep_line" | awk '{print $2}')
dep_new=$(echo "$dep_line" | awk '{print $4}') # field 3 is "->"
# Skip the package we're intentionally upgrading.
[[ "$dep_name" == "$pkg" ]] && continue
dep_old_mm=$(echo "$dep_old" | cut -d. -f1,2)
dep_new_mm=$(echo "$dep_new" | cut -d. -f1,2)
if [[ "$dep_old_mm" != "$dep_new_mm" ]]; then
non_patch_deps+=("$dep_name ($dep_old -> $dep_new)")
else
patch_deps+=("$dep_name ($dep_old -> $dep_new)")
fi
done < <(echo "$dry_run" | grep -E '^[a-zA-Z@][^ ]* [0-9].*->')
if [ ${#patch_deps[@]} -gt 0 ] || [ ${#non_patch_deps[@]} -gt 0 ]; then
echo " Upgrading $pkg will also upgrade these dependent packages:"
for d in "${patch_deps[@]}"; do
echo " [patch] $d"
done
for d in "${non_patch_deps[@]}"; do
echo " [NON-PATCH] $d <-- minor/major version bump"
done
fi
if [ ${#non_patch_deps[@]} -gt 0 ]; then
echo ""
read -p " $pkg has non-patch dependent upgrades. Proceed anyway? (y/N): " pkg_confirm
if [[ ! "$pkg_confirm" =~ ^[Yy]$ ]]; then
echo " Skipping $pkg."
continue
fi
fi
echo " Upgrading $pkg..."
brew upgrade "$pkg"
done
echo ""
echo "Done."
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment