Homebrew needs no introduction on macOS. For formulae, the classic CLI tools, brew update && brew upgrade has been working reliably for years. Casks, the GUI applications, were a different story for me: some apps got upgraded by brew upgrade, others did not, and App Store apps bypassed Homebrew entirely.
Today I stumbled across two projects that close exactly this gap:
- buo/homebrew-cask-upgrade –
brew cu, an upgrade command specifically for casks - mas-cli/mas – a CLI for the Mac App Store
I tested both right away. Below are the relevant parts of the output.
Starting Point: brew update vs. brew upgrade
A small classic first: brew update refreshes Homebrew itself and the tap metadata, brew upgrade upgrades the installed packages. If you mix them up, Homebrew now corrects you:
samuel.heinrich@ ~ % brew update claude-code
Warning: Use `brew upgrade claude-code` to upgrade formulae; running it instead.
==> Downloading Homebrew API data
==> Would upgrade 1 requested outdated package
claude-code 2.1.236 -> 2.1.267
==> Fetching downloads for: claude-code
✔︎ Cask claude-code (2.1.267) Downloaded 200.5MB/200.5MB
==> Upgrading claude-code
2.1.236 -> 2.1.267
==> Unlinking Binary '/opt/homebrew/bin/claude'
==> Linking Binary 'claude' to '/opt/homebrew/bin/claude'
🍺 claude-code was successfully upgraded!
Then the usual full run:
samuel.heinrich@ ~ % brew update && brew upgrade
==> Updating Homebrew...
Already up-to-date.
[...]
Homebrew will now attempt to upgrade casks with `auto_updates true`.
Disable this behaviour with `HOMEBREW_NO_UPGRADE_AUTO_UPDATES_CASKS=1`.
Some casks with `auto_updates true` or `version :latest` may still require `--greedy`,
`HOMEBREW_UPGRADE_GREEDY` or `HOMEBREW_UPGRADE_GREEDY_CASKS` to be upgraded.
==> Would upgrade 35 outdated packages
readline 8.3.3 -> 8.3.6 (757KB)
docker 29.8.0 -> 29.8.1 (9.9MB)
gh 2.100.0 -> 2.101.0 (14MB)
mariadb 12.3.3 -> 13.0.2 (63.5MB)
[...]
bitwarden 2026.8.0 -> 2026.9.0
docker-desktop 4.90.0,238679 -> 4.91.0,239619
google-chrome 153.0.8010.37 -> 153.0.8010.53
iterm2 3.7.1 -> 3.7.2
tunnelblick 8.0.3,6303 -> 9.0.1,6491
visual-studio-code 1.137.0 -> 1.138.0
zulu 26.0.2.1,26.32.203 -> 27.0.0,27.28.101
==> 3 Deprecated or disabled packages
angry-ip-scanner (disabled)
microsoft-remote-desktop (disabled)
zenmap (disabled)
==> Do you want to proceed with the upgrade? [y/n]
The hint about auto_updates true is the core of the problem. Many casks carry this flag because the app ships its own update mechanism (Sparkle, Microsoft AutoUpdate, the Chrome updater, and so on). Homebrew holds back on these apps and leaves the update to the app itself. The result: you end up with two categories of apps on your Mac with different update behaviour, and without --greedy the brew upgrade output never shows you the full picture.
Pitfall: App Management Permission
One cask failed during the run:
==> Upgrading tunnelblick
8.0.3,6303 -> 9.0.1,6491
/opt/homebrew/Library/Homebrew/utils/fork.rb:35:in 'IO#close': Broken pipe (Errno::EPIPE)
[...]
Error: tunnelblick: Cannot change the ownership of '/Applications/Tunnelblick.app' because your terminal does not have App Management permissions.
macOS prevents modifying apps without these permissions, even when using `sudo`.
To fix this, approve the permissions prompt (if one was just shown) or go to
System Settings → Privacy & Security → App Management and add or enable your terminal.
Then run this command again.
The error message is self-explanatory, but the Ruby stack trace before it is distracting at first. The cause is not Homebrew but the macOS App Management permission for the terminal. Without it, the terminal is not allowed to replace apps in /Applications, not even with sudo. The permission is granted per terminal app under System Settings → Privacy & Security → App Management.
In the end, 34 of 35 packages were up to date.
brew-cask-upgrade: brew cu
The project buo/homebrew-cask-upgrade adds the command brew cu to Homebrew. It compares the installed version with the current version for every installed cask, shows the result as a table, and offers to upgrade directly.
Installation
The project is added as a tap. What is new here is Homebrew’s trust mechanism: third-party taps must be trusted explicitly, otherwise their formulae, casks and commands are ignored.
samuel.heinrich@ ~ % brew tap buo/cask-upgrade
==> Tapping buo/cask-upgrade
Cloning into '/opt/homebrew/Library/Taps/buo/homebrew-cask-upgrade'...
Warning: Skipping buo/cask-upgrade because it is not trusted. Run `brew trust buo/cask-upgrade` to trust it.
Tapped 2 commands (40 files, 403.4KB).
samuel.heinrich@ ~ % brew trust buo/cask-upgrade
Trusted tap: buo/cask-upgrade
The command is then brew cu. There is no standalone brew-cask-upgrade binary.
First Run
samuel.heinrich@ ~ % brew cu
==> Options
Include auto-update (-a): false
Include latest (-f): false
==> Updating Homebrew
==> Updating Homebrew...
Already up-to-date.
==> Finding outdated apps
Cask Current Latest A/U Result
1/54 airtool 2.5.3 2.7.9 Y [ PASS ]
2/54 alfred 5.8 5.8 Y [ PASS ]
3/54 angry-ip-scanner 3.9.3 3.9.3 [ OK ]
[...]
7/54 balenaetcher 2.1.6 2.1.7 [OUTDATED]
[...]
23/54 google-drive 88.0.0 131.0.2 Y [ PASS ]
[...]
31/54 microsoft-office 16.83.24031120 16.113.26091740 Y [ PASS ]
[...]
46/54 telegram 10.9 12.10 Y [ PASS ]
[...]
48/54 tunnelblick 8.0.3 9.0.1 Y [ PASS ]
[...]
54/54 zulu 27.0.0 27.0.0 [ OK ]
==> Found outdated apps
Cask Current Latest A/U Result
1/1 balenaetcher 2.1.6 2.1.7 [OUTDATED]
Do you want to upgrade 1 app or enter [i]nteractive mode [y/i/N]? y
==> Upgrading balenaetcher to 2.1.7
[...]
🍺 balenaetcher was successfully installed!
The table is the real added value. The columns:
- Current / Latest – installed version according to Homebrew and current version of the cask definition
- A/U –
Yif the cask is flagged withauto_updates true - Result –
OK(up to date),OUTDATED(offered for upgrade) orPASS(skipped)
PASS means: the cask is outdated but is not touched by default because of auto_updates true. These are exactly the apps (Google Drive, Microsoft Office, Telegram, Tunnelblick, etc.) you never see in brew upgrade. With brew cu -a they are upgraded as well, and -f additionally includes casks with version :latest.
Interesting detail: brew cu flagged balenaetcher as outdated even though it did not appear in the previous brew upgrade run. I did not investigate why Homebrew itself did not report the cask as outdated.
App Store: mas
The second part of the gap is apps from the Mac App Store. brew cu can include them in the same run with --include-mas, but requires mas for that:
samuel.heinrich@ ~ % brew cu --include-mas
Error: In order to use --include-mas the mas-cli has to be installed. Please see the instructions here: https://github.com/mas-cli/mas
samuel.heinrich@ ~ % brew install mas
==> Would install 1 formula:
mas 7.0.0
==> Pouring mas--7.0.0.arm64_tahoe.bottle.tar.gz
🍺 /opt/homebrew/Cellar/mas/7.0.0: 10 files, 11MB
mas is a regular formula and can also be used on its own, for example with mas outdated and mas upgrade. Combined with brew cu, the run looks like this:
samuel.heinrich@ ~ % brew cu --include-mas
==> Options
Include auto-update (-a): false
Include latest (-f): false
Include mac app store (--include-mas): true
==> Updating Homebrew
==> Updating Homebrew...
Already up-to-date.
==> Finding outdated apps
Cask Current Latest A/U Result
[...]
22/76 dot1xprofile 2.3.0 2.4.0 [OUTDATED]
[...]
26/76 eaptest 5.3.0 5.4.0 [OUTDATED]
[...]
32/76 icmputil 3.4.0 3.5.0 [OUTDATED]
[...]
49/76 numbers 14.5 14.5 [ OK ]
[...]
70/76 whatsapp 26.35.74 26.36.74 [OUTDATED]
[...]
74/76 xnip 2.4.3 2.5.0 [OUTDATED]
[...]
==> Found outdated apps
Cask Current Latest A/U Result
1/5 dot1xprofile 2.3.0 2.4.0 [OUTDATED]
2/5 eaptest 5.3.0 5.4.0 [OUTDATED]
3/5 icmputil 3.4.0 3.5.0 [OUTDATED]
4/5 whatsapp 26.35.74 26.36.74 [OUTDATED]
5/5 xnip 2.4.3 2.5.0 [OUTDATED]
Do you want to upgrade 5 apps or enter [i]nteractive mode [y/i/N]? y
==> Upgrading 5 apps
Password:
==> Downloading Dot1xProfile (2.4.0)
==> Downloaded Dot1xProfile (2.4.0)
==> Updating Dot1xProfile (2.4.0)
==> Updated Dot1xProfile (2.4.0) in /Applications/Dot1xProfile.app
[...]
==> Updated Xnip - Screenshot & Annotation (2.5.0) in /Applications/Xnip.app
The list grows from 54 to 76 entries. The App Store apps show up without hyphens in their names (dot1xprofile, wifi signal, the unarchiver) and without an A/U flag. Five of them were outdated and were upgraded directly via mas. Of all things, the network tools Dot1xProfile, EAPTest and ICMPUtil are the ones I would probably not have touched through the App Store for a while longer.
Also visible along the way: mas warns when an upgraded app is not in the Spotlight index and triggers indexing itself. If you do not want that, set MAS_NO_AUTO_INDEX=1.
Conclusion
With brew cu and mas, the complete software state of a Mac can be upgraded from a single terminal. My routine now looks like this:
brew update && brew upgrade
brew cu --include-mas
The first command covers formulae and the regular casks, the second one covers the casks Homebrew does not report as outdated, plus the App Store apps. If you also want to upgrade apps with their own update mechanism through Homebrew, add -a.
Two things should be in place beforehand:
- grant the terminal the App Management permission, otherwise cask upgrades fail like Tunnelblick did
- trust the tap
buo/cask-upgradewithbrew trust, otherwise thebrew cucommand is ignored
That closes the gap between brew upgrade, self-updating apps and the App Store for me.
Download als PDF File