Why Automate Homebrew Package Updates Securely
Running brew update and brew upgrade by hand works fine when you remember to do it. In practice, most Mac users don’t check for weeks or months at a time, leaving formulae and casks with known vulnerabilities unpatched. Automating the process removes that gap, but doing it safely, with logging, error handling, and a way to see what changed, matters as much as the automation itself.
Building a Homebrew Auto Update Script with Bash
Start with a script that captures a full log of what happens during each run.
#!/bin/bash
# Homebrew Auto-Update Script
# Logs all changes and enables rollback
BACKUP_DIR="$HOME/.homebrew_backups"
LOG_FILE="$BACKUP_DIR/brew_update_$(date +%Y%m%d_%H%M%S).log"
# Create backup directory if it doesn't exist
mkdir -p "$BACKUP_DIR"
# Capture current state
echo "=== Homebrew Update Log ===" > "$LOG_FILE"
echo "Start time: $(date)" >> "$LOG_FILE"
echo "" >> "$LOG_FILE"
# List installed packages before update
echo "Installed packages before update:" >> "$LOG_FILE"
brew list >> "$LOG_FILE"
echo "" >> "$LOG_FILE"
# Run update and upgrade
echo "Running brew update..." >> "$LOG_FILE"
brew update >> "$LOG_FILE" 2>&1
echo "Running brew upgrade..." >> "$LOG_FILE"
brew upgrade >> "$LOG_FILE" 2>&1
# Capture state after update
echo "" >> "$LOG_FILE"
echo "Installed packages after update:" >> "$LOG_FILE"
brew list >> "$LOG_FILE"
# Run brew doctor to check for issues
echo "" >> "$LOG_FILE"
echo "System health check:" >> "$LOG_FILE"
brew doctor >> "$LOG_FILE" 2>&1
echo "Update completed at: $(date)" >> "$LOG_FILE"
The script above creates timestamped logs in a backup directory, making it easy to review what changed and when. This is critical for security auditing, you can see exactly which versions were installed and identify when a problematic update occurred.
Add error handling to prevent broken states. If something fails during the upgrade process, you want to know about it immediately rather than discovering the problem hours later.
#!/bin/bash
set -e # Exit on any error
BACKUP_DIR="$HOME/.homebrew_backups"
LOG_FILE="$BACKUP_DIR/brew_update_$(date +%Y%m%d_%H%M%S).log"
ERROR_LOG="$BACKUP_DIR/brew_errors.log"
mkdir -p "$BACKUP_DIR"
# Function to log errors
log_error() {
echo "ERROR: $1" | tee -a "$ERROR_LOG"
echo "ERROR: $1" >> "$LOG_FILE"
}
# Function to send notification (optional)
send_notification() {
osascript -e "display notification \"$1\" with title \"Homebrew Update\""
}
# Trap errors
trap 'log_error "Update failed at line $LINENO"; send_notification "Homebrew update failed"; exit 1' ERR
# Proceed with update
brew update >> "$LOG_FILE" 2>&1 || log_error "brew update failed"
brew upgrade >> "$LOG_FILE" 2>&1 || log_error "brew upgrade failed"
# Cleanup
brew cleanup >> "$LOG_FILE" 2>&1
send_notification "Homebrew update completed successfully"
This version includes trap handling that catches errors and sends you a notification. The set -e flag ensures the script stops immediately if any command fails, preventing cascading problems.
brew upgrade without first understanding what packages depend on each other. A single outdated formula can break multiple applications. Always check brew deps before upgrading critical packages like Python, Node, or Ruby.Scheduling Unattended Upgrades with Launchd
Scheduling unattended upgrades with launchd, macOS’s native task scheduler, removes the need to remember when to update. Launchd is more reliable than cron for macOS because it respects system sleep states and integrates with macOS’s security model. Version Tracker can help you track which packages are running outdated versions, giving you visibility into what your launchd automation is actually updating.
Create a launchd plist file that runs your Homebrew update script on a schedule. The plist format is XML-based and tells launchd exactly when and how to execute your script.
<?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>com.local.homebrew.autoupdate</string>
<key>ProgramArguments</key>
<array>
<string>/bin/bash</string>
<string>/usr/local/bin/homebrew_update.sh</string>
</array>
<key>StartInterval</key>
<integer>86400</integer>
<key>StandardOutPath</key>
<string>/var/log/homebrew_update.log</string>
<key>StandardErrorPath</key>
<string>/var/log/homebrew_update_error.log</string>
<key>RunAtLoad</key>
<true/>
</dict>
</plist>
The StartInterval key specifies how often the job runs, in seconds. 86400 seconds equals 24 hours. For more granular control, use StartCalendarInterval to specify exact times:
<key>StartCalendarInterval</key>
<array>
<dict>
<key>Hour</key>
<integer>2</integer>
<key>Minute</key>
<integer>0</integer>
</dict>
</array>
This example runs the script at 2:00 AM every day. Running updates during off-hours reduces the chance of interrupting your work or causing issues during active development.
Save this plist file to ~/Library/LaunchAgents/com.local.homebrew.autoupdate.plist. Then load it with launchctl:
launchctl load ~/Library/LaunchAgents/com.local.homebrew.autoupdate.plist
Verify the job is loaded by listing all active launchd jobs:
launchctl list | grep homebrew
To unload the job (stop it from running), use:
launchctl unload ~/Library/LaunchAgents/com.local.homebrew.autoupdate.plist
Monitor the logs to ensure the script is running correctly. Check /var/log/homebrew_update.log to see what happened during each run.
| Schedule Type | Best For | Interval | Command |
|---|---|---|---|
| StartInterval | Simple daily/weekly updates | 86400 seconds (daily) | launchctl load |
| StartCalendarInterval | Specific times (2 AM daily) | Hour + Minute keys | launchctl load |
| Cron job | Legacy workflows | Flexible (minute-based) | crontab -e |
Homebrew Security Best Practices for Automated Updates
Automating homebrew package updates securely requires understanding the security risks that come with unattended package installation. When updates run automatically, you lose the opportunity to review what changed before installation. This creates a window where a compromised formula or malicious dependency could affect your system without your immediate knowledge.
The first security practice is to verify that Homebrew itself is secure. Check the integrity of your Homebrew installation:
brew doctor
This command scans for common configuration problems and security issues. Run it before and after any major updates to catch problems early.
Implement a formula verification step in your automation script. Homebrew formulas are stored in public repositories, and while the Homebrew team maintains security standards, it’s worth checking what’s being installed:
# List outdated packages before upgrading
brew outdated
# Show what will be upgraded
brew upgrade --dry-run
The --dry-run flag shows exactly what would be upgraded without actually installing anything. Include this in your logging to create an audit trail:
echo "Packages that would be upgraded:" >> "$LOG_FILE"
brew upgrade --dry-run >> "$LOG_FILE"
For formula management, understand the difference between Homebrew formulae and casks. Formulae are command-line tools and libraries installed from source or precompiled binaries. Casks are GUI applications. They have different update mechanisms and security considerations.
Formulae are typically safer to auto-update because they’re smaller, have fewer dependencies, and the Homebrew community reviews them. Casks are larger applications that may have their own update mechanisms. Consider excluding casks from automatic updates:
# Update only formulae, not casks
brew update
brew upgrade --formulae
Implement security auditing by checking for known vulnerabilities. While Homebrew doesn’t have built-in CVE checking, you can integrate external tools:
# Check for outdated packages with potential security issues
brew outdated --verbose
The --verbose flag shows the current version and the available version, letting you assess whether the update addresses a known vulnerability.
Create a notification system that alerts you to critical updates. If a formula has a security patch, you should know about it immediately rather than waiting for your scheduled update window:
#!/bin/bash
# Check for critical updates
CRITICAL_PACKAGES=("openssl" "curl" "git" "python")
for package in "${CRITICAL_PACKAGES[@]}"; do
if brew outdated | grep -q "^$package"; then
osascript -e "display notification \"Critical update available: $package\" with title \"Homebrew Security Alert\""
fi
done
Frequently Asked Questions
How do I automate Homebrew package updates without manual intervention?
Create a bash script that runs brew update and brew upgrade commands, then schedule it using launchd (macOS’s background process manager). A plist file defines when the script runs. Add error handling and logging to track success or failures. For comprehensive monitoring across all 100,000+ packages on macOS, consider tools like Version Tracker that consolidate updates from 47 different sources, eliminating the need to manage multiple package managers separately.
What are the security risks of automating Homebrew updates?
Automated updates can introduce breaking changes to critical dependencies without warning, potentially disrupting development environments. Mitigate this by auditing your automation scripts for permissions, implementing rollback strategies that capture package versions before upgrades, and using security-focused tools that track CVE databases and macOS Security Advisories. Always test updates in non-production environments first and maintain version control of your Homebrew configuration.
How do I set up launchd to run Homebrew updates in the background?
Create a plist file in ~/Library/LaunchAgents/ that specifies your bash script path, run schedule, and environment variables. Use launchctl load to activate it. The plist defines the Label (unique identifier), ProgramArguments (script path), StartInterval (frequency in seconds), and StandardOutPath/StandardErrorPath (logging). Test with launchctl start before relying on scheduled execution. Ensure your script handles password prompts, use sudo with NOPASSWD configuration if needed.
Should I automate formula updates differently than cask updates?
Yes. Formulas (CLI tools and libraries) often have critical security patches and should update frequently, while casks (GUI applications) may require manual verification to ensure compatibility. Separate your automation: run brew upgrade for formulas on a tight schedule (daily or weekly), and schedule brew upgrade --cask less frequently or with additional notification systems. This prevents unexpected application changes while maintaining security for development dependencies.