Mac Dev Environment Weekly Maintenance — Update brew · Toolchains · SDKs with One Command
A single weekly shell script that updates Homebrew, Mac App Store, npm globals, rustup, cargo binaries, pipx, Flutter, CocoaPods, and oh-my-zsh.
You spend time setting up a new machine, then forget to keep it updated. Two months later, a single brew install drags 30 transitive deps along, native builds break on SDK mismatches, and your security certificates (ca-certificates) silently drift toward expiration.
This article is the 9-step integrated script that prevents that. Run it once a week — done.
TL;DR
- Runs
brew+mas+npm -g+rustup+cargo install-update+pipx+flutter+pod+omzin order - Each step auto-skips if the tool isn't installed, and a failure in one doesn't stop the rest
- Ends with a notice-only macOS system update check (no auto install — reboots are too risky)
- ~100 lines of plain bash
Why Bother
1. Certificate Expiration
ca-certificates ships a new bundle every quarter. Skip two quarters and some HTTPS calls start failing. You may think you don't care, but npm registry, Homebrew itself, and the GitHub API are all affected.
2. Mobile Build SDK Drift
Especially risky on Flutter/iOS machines. Flutter stable ships 3–4 times a month; CocoaPods' spec repo, if unsynced for a week, starts throwing "spec not found" during pod install. A weekly pod repo update alone blocks 80% of this pain.
3. Language Toolchain Accumulation
Globals installed via rustup, pipx, or cargo each need their own update. Forget, and one day you discover cargo install won't fetch the latest because of rust-version mismatch.
Prerequisites
- macOS 12+ + Homebrew (Mac initial setup)
- For the full 9 steps:
mas,pipx,cargo-update,flutter,cocoapods,oh-my-zshpre-installed - Missing some tools is fine — those steps SKIP gracefully
One-time prep
brew install mas pipx cocoapods
brew install --cask flutter
cargo install cargo-update
sh -c "$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)"
pipx ensurepathcargo install cargo-update compiles, so the first run takes 2–3 minutes. Flutter is ~1GB to download.
9 Update Steps
| # | Step | Command | Notes |
|---|---|---|---|
| 1 | Homebrew | brew update && brew upgrade && brew cleanup | formulae + casks |
| 2 | Mac App Store | mas upgrade | App Store apps (Xcode etc.) |
| 3 | npm globals | npm update -g | global npm packages |
| 4 | Rust toolchain | rustup update | stable channel |
| 5 | Cargo binaries | cargo install-update -a | requires cargo-update extension |
| 6 | pipx | pipx upgrade-all | Python global tools |
| 7 | Flutter SDK | flutter upgrade | current channel's latest |
| 8 | CocoaPods repo | pod repo update | iOS build spec sync |
| 9 | oh-my-zsh | ~/.oh-my-zsh/tools/upgrade.sh | zsh framework |
The Integrated Script
Save as e.g. ~/bin/update-system.sh. chmod +x and use.
#!/usr/bin/env bash
#
# Bulk system-tool update — macOS
# Usage: ./update-system.sh
#
# Scope:
# 1. Homebrew (formulae + casks)
# 2. Mac App Store (mas, if installed)
# 3. npm globals
# 4. Rust toolchain (rustup)
# 5. Cargo binaries (cargo install-update -a, needs cargo-update)
# 6. pipx (Python global tools)
# 7. Flutter SDK (flutter upgrade)
# 8. CocoaPods repo (pod repo update — iOS build dep)
# 9. oh-my-zsh (omz update)
#
# Ends with `softwareupdate -l` notice (no auto-install — reboot risk)
#
set -uo pipefail
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[0;33m'
CYAN='\033[0;36m'
GRAY='\033[0;90m'
NC='\033[0m'
TOTAL=9
STEP=0
step() {
STEP=$((STEP + 1))
printf "\n${CYAN}[%d/%d] %s${NC}\n" "$STEP" "$TOTAL" "$1"
}
ok() { printf " ${GREEN}OK${NC} %s\n" "$1"; }
skip() { printf " ${GRAY}SKIP${NC} %s\n" "$1"; }
warn() { printf " ${YELLOW}WARN${NC} %s\n" "$1"; }
fail() { printf " ${RED}FAIL${NC} %s\n" "$1"; }
printf "${CYAN}━━━ System update (macOS) ━━━${NC}\n"
# Initialize fnm so npm step can find Node
if command -v fnm >/dev/null 2>&1; then
eval "$(fnm env --use-on-cd --shell bash 2>/dev/null)" 2>/dev/null || true
fi
# 1. Homebrew
step "Homebrew (formulae + casks)"
if command -v brew >/dev/null 2>&1; then
if brew update && brew upgrade && brew cleanup; then
ok "brew update / upgrade / cleanup"
else
fail "brew step partially failed — check log above"
fi
else
skip "brew not installed"
fi
# 2. Mac App Store
step "Mac App Store (mas)"
if command -v mas >/dev/null 2>&1; then
mas upgrade && ok "mas upgrade" || fail "mas upgrade"
else
skip "mas not installed (brew install mas)"
fi
# 3. npm globals
step "npm globals"
if command -v npm >/dev/null 2>&1; then
npm update -g && ok "npm update -g" || fail "npm update -g"
else
skip "npm not installed"
fi
# 4. Rust toolchain
step "Rust toolchain (rustup)"
if command -v rustup >/dev/null 2>&1; then
rustup update && ok "rustup update" || fail "rustup update"
else
skip "rustup not installed"
fi
# 5. Cargo binaries
step "Cargo binaries (cargo install-update -a)"
if command -v cargo >/dev/null 2>&1; then
if cargo install-update --version >/dev/null 2>&1; then
cargo install-update -a && ok "cargo install-update -a" || fail "cargo install-update -a"
else
warn "cargo-update missing (install: cargo install cargo-update)"
fi
else
skip "cargo not installed"
fi
# 6. pipx
step "pipx (Python global tools)"
if command -v pipx >/dev/null 2>&1; then
pipx upgrade-all && ok "pipx upgrade-all" || fail "pipx upgrade-all"
else
skip "pipx not installed (brew install pipx)"
fi
# 7. Flutter SDK
step "Flutter SDK (flutter upgrade)"
if command -v flutter >/dev/null 2>&1; then
flutter upgrade && ok "flutter upgrade" || fail "flutter upgrade"
else
skip "flutter not installed"
fi
# 8. CocoaPods repo
step "CocoaPods repo (pod repo update)"
if command -v pod >/dev/null 2>&1; then
pod repo update && ok "pod repo update" || fail "pod repo update"
else
skip "pod not installed (brew install cocoapods)"
fi
# 9. oh-my-zsh
step "oh-my-zsh (omz update)"
if [ -d "$HOME/.oh-my-zsh" ]; then
OMZ_UPDATER="$HOME/.oh-my-zsh/tools/upgrade.sh"
if [ -x "$OMZ_UPDATER" ]; then
zsh "$OMZ_UPDATER" && ok "oh-my-zsh updated" || fail "oh-my-zsh"
else
skip "oh-my-zsh upgrade.sh missing"
fi
else
skip "oh-my-zsh not installed"
fi
# macOS system update — notice only
printf "\n${CYAN}━━━ macOS system update check (notice only) ━━━${NC}\n"
if command -v softwareupdate >/dev/null 2>&1; then
SU_OUT=$(softwareupdate -l 2>&1)
if echo "$SU_OUT" | grep -qi "no new software\|No updates"; then
printf "${GRAY} Up to date — no system updates${NC}\n"
else
printf "${YELLOW} ⚠ System updates available:${NC}\n"
echo "$SU_OUT" | sed 's/^/ /'
printf "${GRAY} Install with: sudo softwareupdate -ia --restart (will reboot)${NC}\n"
fi
fi
printf "\n${CYAN}━━━ Done ━━━${NC}\n"
printf "${GRAY}Checks: brew doctor / brew outdated / npm outdated -g / flutter doctor${NC}\n"Example Output
The first run is long (5 minutes if brew has 13 upgrades pending). Subsequent runs are 1–2 minutes.
━━━ System update (macOS) ━━━
[1/9] Homebrew (formulae + casks)
==> Upgrading 13 outdated packages:
ca-certificates 2026-03-19 -> 2026-05-14
ruby 4.0.3 -> 4.0.4
sqlite 3.53.0 -> 3.53.1
python@3.14 3.14.4_1 -> 3.14.5
...
==> This operation has freed approximately 18.8MB of disk space.
OK brew update / upgrade / cleanup
[2/9] Mac App Store (mas)
OK mas upgrade
[3/9] npm globals
OK npm update -g
[4/9] Rust toolchain (rustup)
stable-aarch64-apple-darwin unchanged - rustc 1.95.0
OK rustup update
...
[9/9] oh-my-zsh (omz update)
Hooray! Oh My Zsh has been updated!
OK oh-my-zsh updated
━━━ macOS system update check (notice only) ━━━
Up to date — no system updates
━━━ Done ━━━
Automation — How Far?
Full automation is not recommended. brew major bumps (e.g., vercel-cli 53→54) occasionally ship breaking changes that you want to notice the moment they land.
A reasonable middle ground is a reminder, not an auto-run:
~/Library/LaunchAgents/local.update-reminder.plist:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key><string>local.update-reminder</string>
<key>ProgramArguments</key>
<array>
<string>osascript</string>
<string>-e</string>
<string>display notification "Weekly update-system check" with title "Dev Maintenance"</string>
</array>
<key>StartCalendarInterval</key>
<dict>
<key>Weekday</key><integer>5</integer>
<key>Hour</key><integer>10</integer>
<key>Minute</key><integer>0</integer>
</dict>
</dict>
</plist>Activate:
launchctl load -w ~/Library/LaunchAgents/local.update-reminder.plistEvery Friday at 10:00, a single desktop notification. You decide when to run.
Gotchas
- No auto-install for macOS system updates:
softwareupdate -ia --restartreboots forcibly. Your IDE, terminals, docker containers — all gone. The script only lists available updates; you install on your terms. pod repo updateis slow (5+ minutes possible): the trunk repo is huge. Longer if you've skipped many weeks.oh-my-zshhas its own auto-update option:.zshrcsettingzstyle ':omz:update' mode auto. Harmless overlap with this script.- Cargo compiles:
cargo install-update -acompiles fresh versions of installed binaries — first run can take minutes.
Windows Users?
Same idea in PowerShell, covering MSYS2 + winget + language toolchains → Windows dev environment weekly maintenance.
Summary
- One-by-one updates → you forget
- Single integrated script → forget all you want, just run it
- Automation only as far as reminder, never auto-run
- Once a week prevents brew dep storms, SDK drift, and certificate expiry