Distributing your own scripts via Homebrew
I use Homebrew all the time. Whenever I see a new CLI that offers an npm
or uv
install path alongside a brew
one, I choose brew every single time.
And yet, when it comes time to publish a CLI of my own, I usually just ship it as a Ruby gem or an npm package, because I had (and have!) no fucking clue how Homebrew works. I'm not enough of a neckbeard to peer behind the curtain as soon as root directories like /usr
and /opt
are involved, so I never bothered before today.
But it's 2025 and we can consult LLMs to conjure whatever arcane incantations we need. And because he listens to the cast, I can always fall back on texting Mike McQuaid when his docs suck.
So, because I'll never remember any of this shit (it's already fading from view as I type this), below are the steps involved in publishing your own CLI to Homebrew. The first formula I published is a simple Ruby script, but this guide should be generally applicable.
Glossary
Because Homebrew really fucking leans in to the whole "home brewing as in beer" motif when it comes to naming, it's easy to get lost in the not-particularly-apt nomenclature they chose.
Translate these in your head when you encounter them:
- Formula → Package definition
- Tap → Git repository of formulae
- Cask → Manifest for installing pre-built GUIs or large binaries
- Bottle → Pre-built binary packages that are "poured" (copied) instead of built from source
- Cellar → Directory containing your installed formulae (e.g.
/opt/homebrew/Cellar
) - Keg → Directory housing an installed formula (e.g.
Cellar/foo/1.2.3
)
Overview
First thing to know is that the Homebrew team doesn't want your stupid CLI in the core repository.
Instead, the golden path for us non-famous people is to:
- Make your CLI, push it to GitHub, cut a tagged release
- Create a Homebrew tap
- Create a Homebrew formula
- Update the formula for each CLI release
After you complete the steps outlined below, users will be able to install your cool CLI in just two commands:
brew tap your_github_handle/tap
brew install your_cool_cli
Leaving the "make your CLI" step as an exercise for the reader, let's walk through the three steps required to distribute it on Homebrew. In my case, I slopped up a CLI called imsg that creates interactive web archives from an iMessage database.
Create your tap
Here's Homebrew's guide on creating a tap. Let's follow along how I set things up for myself. Just replace each example with your own username or organization.
For simplicity's sake, you probably want a single tap for all the command line tools you publish moving forward. If that's the case, then you want to name the tap homebrew-tap
. The homebrew
prefix is treated specially by the brew
CLI and the tap
suffix is conventional.
First, create the tap:
brew tap-new searlsco/homebrew-tap
This creates a scaffold in /opt/homebrew/Library/Taps/searlsco/homebrew-tap
. Next, I created a matching repository in GitHub and pushed what Homebrew generated:
cd /opt/homebrew/Library/Taps/searlsco/homebrew-tap
git remote add origin git@github.com:searlsco/homebrew-tap.git
git push -u origin main
Congratulations, you're the proud owner of a tap. Now other homebrew users can run:
brew tap searlsco/tap
It doesn't contain anything useful, but they can run it. The command will clone your repository into their /opt/homebrew/Library/Taps
directory.
Create your formula
Even though Homebrew depends on all manner of git operations to function and fully supports just pointing your formula at a GitHub repository, the Homebrew team recommends instead referencing versioned tarballs with checksums. Why? Something something reproducibility, yadda yadda open source supply chain. Whatever, let's just do it their way.
One nifty feature of GitHub is that they'll host a tarball archive of any tags you push at a predictable URL. That means if I run these commands in the imsg repository:
git tag v0.0.5
git push --tags
Then GitHub will host a tarball at github.com/searlsco/imsg/archive/refs/tags/v0.0.5.tar.gz.
Once we have that tarball URL, we can use brew create
to generate our formula:
brew create https://github.com/searlsco/imsg/archive/refs/tags/v0.0.5.tar.gz --tap searlsco/homebrew-tap --set-name imsg --ruby
The three flags there do the following:
--tap
points it to the custom tap we created in the previous step, and will place the formula in/opt/homebrew/Library/Taps/searlsco/homebrew-tap/Formula
--set-name imsg
will name the formula explicitly, thoughbrew create
would have inferred this and confirmed it interactively. The name should be unique so you don't do something stupid like make a CLI named TLDR when there's already a CLI named TLDR or a CLI named standard when there's already a CLI named standard--ruby
is one of several template presets provided to simplify the task of customizing your formula
Congratulations! You now have a formula for your CLI. It almost certainly doesn't work and you almost certainly have no clue how to make it work, but it's yours!
This is where LLMs come in.
- Run
brew install --verbose imsg
- Paste what broke into ChatGPT
- Update formula
- GOTO 1 until it works
Eventually, I wound up with a working Formula/imsg.rb file. (If you're publishing a Ruby CLI, feel free to copy-paste it as a starting point.) Importantly, and a big reason to distribute via Homebrew as opposed to a language-specific package manager, is that I could theoretically swap out the implementation for some other language entirely without disrupting users' ability to upgrade.
Key highlights if you're reading the formula contents:
- All formulae are written in Ruby, not just Ruby-related formulae. Before JavaScript and AI took turns devouring the universe, popular developer tools were often written in Ruby and Homebrew is one of those
- You can specify your formula's git repository with the
head
method (though I'm unsure this does anything) - Adding a livecheck seemed easy and worth doing
- Adding a test to ensure the binary runs can be as simple as asserting on help output. Don't let the generated comment scare you off
- Run
brew style searlsco/tap
to make sure you didn't fuck anything up. - By default, the
--ruby
template addsuses_from_macos "ruby"
, which is currently version 2.6.10 (which was released before the Covid pandemic and end-of-life'd over three years ago). You probably want to rely on the ruby formula withdepends_on "ruby@3"
instead
When you're happy with it, just git push
and your formula is live! Now any homebrew user can install your thing:
brew tap searlsco/tap
brew install imsg
Update the formula for each CLI release
Of course, any joy I derived from getting this to work was fleeting, because of this bullshit at the top of the formula:
class Imsg < Formula
url "https://github.com/searlsco/imsg/archive/refs/tags/v0.0.5.tar.gz"
sha256 "e9166c70bfb90ae38c00c3ee042af8d2a9443d06afaeaf25a202ee8d66d1ca04"
Who the fuck's job is it going to be to update these URLs and SHA hashes? Certainly not mine. I barely have the patience to git push
my work, much less tag it. And forget about clicking around to create a GitHub release. Now I need to open a second project and update the version there, too? And compute a hash? Get the fuck out of here.
Now, I will grant that Homebrew ships with a command that opens a PR for each formula update and some guy wrapped it in a GitHub action, but both assume you want to daintily fork the tap and humbly submit a pull request to yourself. Clearly all this shit was designed back when Homebrew was letting anybody spam shit into homebrew-core. It's my tap, just give me a way to commit to main, please and thank you.
So anyway, you can jump through all those hoops each time you update your CLI if you're a sucker. But be honest with yourself, you're just gonna wind up back at this stupid blog post again, because you'll have forgotten the process. To avoid this, I asked my AI companion to add a GitHub workflow to my formula repository that automatically commits release updates to my tap repository.
If you want to join me in the fast lane, feel free to copy paste my workflow as a starting point. The only things you'll need to set up yourself:
- You'll need a personal-access token:
- When creating the PAT, add your
homebrew-tap
repository andContent
→Write
permissions - In the formula repository's settings under
Secrets and variables
→Actions
→Repository secrets
and name itHOMEBREW_TAP_TOKEN
(GitHub docs)
- When creating the PAT, add your
- You'll need to specify the tap and formula environment variables
- You'll probably want to update the GitHub bot account, probably to the GitHub Actions bot if you don't have your own:
GH_EMAIL: 41898282+github-actions[bot]@users.noreply.github.com
GH_NAME: github-actions[bot]
Now, whenever you cut a release, your tap will be updated automatically. Within a few seconds of running git push --tags
in your formula's repositories, your users will be able to upgrade their installations with:
brew update
brew upgrade imsg
That's it. Job's done!
The best part
This was a royal pain in the ass to figure out, so hopefully this guide was helpful. The best part is that once your tap is set up and configured and you have a single working formula to serve as an example, publishing additional CLI tools in the future becomes almost trivial.
Now, will I actually ever publish another formula? Beats me. But it feels nice to know it would only take me a few minutes if I wanted to. 🍻