Homebrew: Upgrading Casks and App Store Apps from the CLI

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:

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 – Y if the cask is flagged with auto_updates true
  • Result – OK (up to date), OUTDATED (offered for upgrade) or PASS (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-upgrade with brew trust, otherwise the brew cu command is ignored

That closes the gap between brew upgrade, self-updating apps and the App Store for me.

Samuel Heinrich
Senior Network Engineer at Selution AG (Switzerland)
Arbeitet in Raum Basel (Switzerland) als Senior Network Engineer mit über 15 Jahren Erfahrung im Bereich Netzwerk

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.