Part 3 of a series on the tools I actually run.

Last time I said Homebrew is what makes a Mac feel like a workstation instead of an appliance. Here’s the long version: how to get it on there, what I install, and the four or five things that have cost me evenings.

What it is

A package manager for macOS. You type brew install ripgrep and ripgrep is on your PATH. That’s it, that’s the whole pitch, and honestly it’s enough.

It deals with two kinds of thing. Formulae are command-line software. Casks are normal Mac applications, the ones that would otherwise show up as a .dmg you have to drag into /Applications. It used to be that these took different commands and you had to remember which was which. Now it’s brew install either way, which is a small mercy.

The bit I actually care about is where it puts everything. On Apple Silicon that’s /opt/homebrew. On the old Intel machines it was /usr/local. Either way it stays out of the system directories, so Homebrew’s Python and Apple’s Python never end up arguing over the same path. This sounds like housekeeping trivia. It isn’t. Anyone who used Homebrew on Intel and watched it scribble all over /usr/local remembers the difference.

Installing it

One line:

/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"

Paste that into Terminal and you’re most of the way there.

A few things about what happens next. It’ll ask for your password, because it needs to create /opt/homebrew and hand you ownership of it. Before that it prints a summary of everything it’s about to do, and you should read it. I know nobody reads these. But you are about to pipe a script off the internet into bash and then give it sudo, and taking twenty seconds to skim what it says it’s doing seems like the least you could do.

If you don’t have the Xcode Command Line Tools, the installer notices and pulls them in for you. That’s a couple of gigabytes and it takes a while, so this is a good moment to go make coffee. (You can run xcode-select --install beforehand if you’d rather do it explicitly. Same result.)

The step everyone misses

Here’s the one that generates all the “homebrew installed but brew: command not found” posts.

On Apple Silicon, /opt/homebrew/bin isn’t on your PATH. The installer does tell you. It’s in a “Next steps” block right at the end, underneath a wall of output, and it is very easy to close the window and walk away.

Run these:

echo 'eval "$(/opt/homebrew/bin/brew shellenv)"' >> ~/.zprofile
eval "$(/opt/homebrew/bin/brew shellenv)"

First line makes it permanent. Second one fixes the shell you’re in right now so you don’t have to open a new window. On Intel this is unnecessary, /usr/local/bin is already there.

Then:

brew doctor

“Your system is ready to brew” and you’re done. If it grumbles, hold off on fixing anything until you’ve read the gotchas at the bottom of this post, because brew doctor complains about a lot of things that are fine.

What I install

Files and moving around

brew install mc tree ncdu eza bat fd ripgrep fzf

mc is Midnight Commander, which I went on about at some length last time. Two panes, function keys along the bottom, the same layout I learned in Norton Commander in about 1994.

ncdu is the one I’d defend hardest here. It shows you what’s eating your disk, sorted, browsable, and you can delete from inside it. Every time a drive fills up this is the first thing I run.

The rest are modern versions of tools you already use. eza instead of ls. bat instead of cat, with syntax highlighting. fd instead of find, with a command line that doesn’t require a man page every time. ripgrep instead of grep, and the binary is rg, which trips people up. fzf is a fuzzy finder that hooks into your shell history and turns Ctrl-R from a thing you tolerate into a thing you use constantly.

I held out against these for years. My argument was that the originals are on every machine everywhere and I didn’t want to get soft. That was dumb. find is still there when I’m SSHed into a box that doesn’t have fd, and in the meantime being fast on my own laptop is worth something.

Editors

brew install nano micro

I don’t use vim. I know. I’ve tried, more than once, and I always end up back somewhere simpler, and at this point I’ve made my peace with it.

macOS already ships nano, but the version Apple includes lags behind, so Homebrew’s is worth having. It does what nano has always done: opens the file, shows you the shortcuts along the bottom, gets out of your way. For editing a config file over SSH it’s perfect and I have never once wanted anything else.

micro is the interesting one. It’s a terminal editor that behaves the way a person coming from a GUI would expect. Ctrl-S saves. Ctrl-Z undoes. Ctrl-C and Ctrl-V do what you think. Mouse support works, including selecting text and clicking to position the cursor. Syntax highlighting out of the box, multiple cursors if you want them, and a plugin system if you get keen. It’s a single Go binary with no dependencies.

The pitch for micro is basically: modern editor conveniences, in a terminal, without learning a modal editing paradigm first. If that sounds like a compromise, it is, and it’s a compromise I make happily several times a day.

Terminal and version control

brew install tmux git gh stow

tmux matters more on the servers than the laptop, but you want the same config both places so it goes on everything. gh is GitHub’s CLI, good for pull requests without a browser. stow symlinks a dotfiles repo into your home directory and it’s the least complicated way I’ve found to do that particular job.

Media

brew install mpv ffmpeg yt-dlp imagemagick

mpv plays video. That’s all it does. No library, no skin, no account, no update nag, no telemetry, no “we’ve redesigned the interface” surprise one morning. It opens the file and plays it, it handles every codec I’ve thrown at it, and the keyboard shortcuts make sense. If you want a Mac-shaped window around the same engine, brew install --cask iina wraps mpv in a native UI.

ffmpeg is the thing all of this is built on. Converting, trimming, extracting audio, remuxing without re-encoding. The command line is notoriously hostile and it is still worth memorising three or four invocations, because the alternative is a web uploader with a watermark.

yt-dlp pulls video from an absurdly long list of sites. imagemagick resizes and converts images in bulk, which turns out to come up constantly once you know it’s there.

Network stuff

brew install nmap mtr iperf3 socat wget rsync

mtr is traceroute and ping at the same time, running continuously. When something on the network is slow and you don’t know where, this tells you which hop is dropping packets. Nothing else gives you that as fast.

iperf3 measures actual throughput between two machines. This is how you find out your gigabit link has quietly negotiated down to 100 megabit because of a bad cable, which happened to me last spring and took embarrassingly long to diagnose.

rsync needs a mention: the one Apple ships is ancient. Homebrew’s is current. If you’re syncing to a Linux box, get the current one.

Watching the machine

brew install htop btop

htop if you like the classic look, btop if you want graphs and colour. Either beats Activity Monitor for actually seeing what’s going on.

The GNU userland

This is the one that catches everybody, and it’s the single most useful thing in this post if you run Linux servers.

brew install coreutils findutils gnu-sed gawk

macOS ships BSD versions of the core utilities. They take different flags than the GNU versions on your servers. So a script you wrote on Debian runs locally and fails in small baffling ways: sed -i wants a different argument, ls --color isn’t a thing, date -d doesn’t exist.

These formulae give you the GNU versions, prefixed with g. So gsed, gfind, gls, gdate. You can add the gnubin directories to your PATH and drop the prefixes, and Homebrew prints exactly which lines to add. I don’t. Doing that changes the behaviour of every script on the machine that calls sed, including ones Apple wrote that expect BSD behaviour, and I’d rather type gsed on purpose than debug that.

Odds and ends

brew install jq pandoc gnupg age watch

jq slices JSON, which is most of what talking to any API involves. pandoc converts between document formats. age is file encryption that fits in one command instead of GPG’s forty, and I reach for it far more often than I expected to.

Applications

Casks install and update GUI apps from the terminal:

brew install --cask ghostty rectangle utm tailscale-app
brew install --cask visual-studio-code obsidian iina keka stats

rectangle does keyboard window snapping, which macOS still won’t do properly on its own. utm runs VMs on Apple Silicon and is where anything that needs a real Linux kernel gets tested. stats puts CPU and network readouts in the menu bar. keka opens archive formats macOS refuses to.

The point isn’t installing them once. It’s that a new machine can install all of them in one go without you visiting fifteen download pages and clicking through fifteen slightly different installers.

Background services

Some formulae come with a service definition and Homebrew will register them with launchd:

brew services start syncthing
brew services list

Handy for things you want always running. Go easy though. Each one is a background process that starts at login and that you will completely forget about, and running brew services list a year later is rarely a pleasant experience.

The Brewfile

This is the part that makes everything above worth doing.

brew bundle dump --file=~/dotfiles/Brewfile

Writes out every formula, cask and tap you have. Commit it with your dotfiles. Then on a new machine:

brew bundle install --file=~/dotfiles/Brewfile

and it all comes back. Setting up a replacement laptop went from a lost afternoon to one command and a coffee. There’s a --cleanup flag that makes the Brewfile authoritative and removes anything not in it, which is either exactly what you want or a disaster, so try it somewhere you don’t care about first.

Upkeep, and the things that got me

The routine:

brew update && brew upgrade
brew autoremove
brew cleanup

update refreshes the catalogue, upgrade upgrades things, autoremove drops orphaned dependencies, cleanup deletes old downloads. That last one frees up a genuinely surprising amount of disk.

Now the parts that have actually bitten me.

brew upgrade upgrades everything. Including whatever you were deliberately holding at a version. brew pin <formula> stops that. Pin before you need to, because the way you find out is a routine Tuesday update breaking the exact thing you needed working that afternoon.

Casks with their own updaters get weird. Apps that update themselves drift away from Homebrew’s idea of what version they are, and plain brew upgrade skips them. brew upgrade --cask --greedy forces the issue. Sometimes that means Homebrew reinstalls an app that had already updated itself. Noisy, harmless.

Don’t develop against Homebrew’s Python. It’s there because other formulae need it, and it moves whenever those formulae move. I had a virtualenv stop working after upgrading something completely unrelated, and it took me an hour to work out why, because nothing in the error message points at the actual cause. Use mise or uv for project Pythons and leave Homebrew’s alone. Same goes for Node.

brew doctor cries wolf constantly. Unlinked kegs, leftover files in /usr/local from an Intel machine you migrated off three years ago, config it merely finds unusual. Most of it doesn’t matter. Read each warning and think about it rather than working down the list fixing things.

PATH isn’t where you think it is. Cron jobs, LaunchAgents, anything not running in a login shell: none of them have /opt/homebrew/bin on the PATH. Use absolute paths in unattended scripts. This one cost me an entire evening on a backup job that ran perfectly when I typed it and silently did nothing on schedule, for about two weeks, before I noticed.

And it still isn’t Linux. The userland is close enough to be useful and not close enough to trust. Different kernel, different init, different filesystem semantics. Anything destined for a server gets tested on a server, or in a VM.

If you only install eight things

brew install mc ripgrep fd bat htop mpv ffmpeg jq

That’s the set I’d notice missing within an hour. Everything else is refinement.


Next in the series: the dotfiles repo that keeps the Mac and the servers in sync.