Sunday evening, AutoBrew had no snapshot engine, no CLI helper, and no health dashboard. By Saturday afternoon, v2.4.0 was out — and somewhere in the middle of those five days I spent a solid three hours fighting Apple’s signing model for a command-line tool that, by definition, cannot carry a provisioning profile. That fight is as much the story as the features are.
This post covers what shipped in AutoBrew v2.4, how the four headline features actually work under the hood, and where I still have reservations about my own decisions.
AutoBrew v2.4: What Actually Changed
Pre-upgrade snapshots — rollback in one click
This is the feature the whole 2.4 release is built around. Before AutoBrew runs brew upgrade on any cask, it now captures a snapshot of that application’s user data first. If the upgrade breaks something — corrupt preferences, a plugin that stops loading, a database migration that goes sideways — you can roll back from the new Update History sidebar entry without touching the terminal.
The snapshot itself is not a full app bundle copy. It targets the parts that brew upgrade never touches but that a bad release can corrupt: ~/Library/Application Support/<AppName>, ~/Library/Preferences/<BundleID>.plist, and any app-specific containers the cask declares. The AppSnapshotEngine serialises those paths into a versioned archive keyed by cask name and timestamp.
What I’m genuinely happy about here is the diff viewer — you can open any two snapshots side by side and see exactly which files changed, which were added, and which disappeared. That turned out more useful during testing than I expected, and I can already see it becoming the thing people actually use day to day rather than just the emergency rollback.
The per-cask pre-snapshot shell hook (you can drop a script at ~/.config/autobrew/hooks/<cask-name>/pre-snapshot.sh) came out of one concrete problem: some apps write to non-standard paths, and I needed a way for power users to extend the snapshot scope without me having to maintain a hardcoded list of edge cases. The hook runs before the archive step and can export additional paths. It’s opt-in, and if the script exits non-zero the snapshot aborts and the upgrade does not proceed. Safe by default.
The selective restore is worth naming separately. Early in the design I had restore as an all-or-nothing operation. That felt wrong the moment I tested it on an app with a large media cache — you almost never want to roll back gigabytes of cached content, you want to roll back the preferences that broke the login flow. So restore is now per-component: you pick which parts of the snapshot to apply, and the rest stays as-is.
A thin CLI that delegates through a URL scheme
I wanted autobrew to work from the terminal — autobrew snapshot, autobrew upgrade --cask firefox, that kind of thing. The obvious path is a full CLI binary that reimplements the same logic as the app. I didn’t want that. Two codepaths for the same operations is how you get drift and bugs.
Instead, the CLI helper is deliberately thin. It takes your command, serialises it into a URL with a custom autobrew:// scheme, and fires that URL at the running app. The app receives it, routes it through the same BrewStore and AppSnapshotEngine it always uses, and the result comes back to the terminal via a local Unix domain socket. If the app isn’t running, the CLI launches it in the background first.
This took me the better part of an evening to get right, partly because the URL encoding for shell arguments with spaces and special characters needed more care than I initially gave it, and partly because the response-back channel via the socket required a small timeout-and-retry loop that I underestimated. The final implementation in AutoBrewCLI/main.swift is about 180 lines — intentionally. I want to be able to read it in one sitting.
The signing situation for this binary is where I lost most of Saturday. A command-line tool cannot carry a provisioning profile. Apple’s toolchain is straightforward about this, but my CI workflow was not. I went through three different configurations in the space of two hours: automatic signing, then ci-open-source@v1 as a shared action, then a manual revert back to Manual+Profile signing with the profile specifier stripped for the CLI target. The commit history shows the full trail of shame. The final setup works, but I’m not fully satisfied — I’d like to consolidate the signing configuration into a single place rather than having the CLI target handled as a special case. That’s next week’s problem.
A brew doctor health section
brew doctor is the command Homebrew users run when something is wrong. It checks for broken symlinks, stale formulae, path inconsistencies, and a dozen other things. The output is plain text and, unless you’ve read it before, not especially easy to parse.
The new Health section in AutoBrew runs brew doctor in the background on launch (and on demand via a toolbar button) and parses the output into categorised warnings with human-readable labels. Each warning gets a severity icon and a one-line explanation of what it means and what to do. The parser in BrewDoctorParser.swift handles the five most common warning categories; anything it doesn’t recognise surfaces as a raw text entry so nothing gets silently dropped.
I deliberately did not try to auto-fix warnings. That felt like overreach. The value here is visibility — knowing your Homebrew installation is healthy without having to remember to run the command.
Orphan cleanup surfaced in the UI
brew autoremove removes packages that were installed as dependencies but are no longer needed. Most people never run it. BrewStore now tracks orphaned packages separately from the main package list, surfaces them as a count badge, and lets you remove them in one click. There’s a confirmation sheet that lists what will be removed before anything happens.
This one was straightforward to implement — brew autoremove --dry-run gives you the list, and the actual removal is a single subprocess call. The only mildly tricky part was updating the package list in real time as the removal runs, which required a bit of care around the @MainActor boundary in BrewStore.
The Signing Detour, and What I’d Do Differently
I want to be honest about the CI signing saga because it’s a good illustration of a recurring macOS development tax. Apple’s signing model makes total sense for app bundles distributed through the App Store. It makes considerably less sense when you’re trying to sign a command-line tool that lives inside that bundle but doesn’t participate in the profile system.
My first instinct was to switch the whole archive step to automatic signing and let Xcode sort it out. That worked locally and failed on CI. Then I tried routing through a shared action (ci-open-source@v1) that I maintain for exactly this kind of setup — and the profile specifier on the CLI target caused the export to fail because tools cannot use profiles. In the end I reverted to Manual+Profile for the app target and simply dropped the profile specifier for the CLI target, which is what the toolchain actually needs.
The lesson I keep re-learning: macOS signing on CI punishes you for any assumption you carry over from local development. The environment is different, the keychain is different, the entitlements resolution is different. I should have tested the export step on CI before I had four features waiting to ship behind it. Next time I’ll isolate the signing configuration first.
If you’re building a macOS app and wondering why signing and CI setup feel disproportionately hard relative to the actual app work — this is why. It’s not just you. It’s a structural cost of the platform, and it’s worth budgeting time for it explicitly. If you’re considering having a macOS app built for your business, I build that cost into the estimate from the start, not as an afterthought.
What’s Next
The retry logic for failed upgrades (exponential backoff at 1h, 4h, and 12h) is already in main and will ship in 2.4.1 alongside whatever I find during the beta period. I want to look at the snapshot storage path — right now it defaults to ~/Library/Application Support/AutoBrew/Snapshots, but external snapshot storage via a security-scoped bookmark is in place and I want to make it easier to configure from onboarding rather than buried in settings.
The CI signing setup needs one more pass. I want the CLI target’s signing configuration to live in a single .xcconfig file rather than being managed as a Xcode project exception. That’s a one-hour job I’ll do at the start of next week before I touch anything else.
AutoBrew v2.4.0 is available on GitHub — download the DMG, drag it to Applications, and the update will find you from there.