
Feel like Becky's podcast is starting to hit its stride. Really enjoyed today's episode.*
*Conflict of interest: I am married to Becky and therefore predisposed to judging it more harshly gram.betterwithbecky.com/podcasts/9
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. 🍻

While I'm complaining about LLMs, another one: the overwhelming preference for creating dead code by keeping around old code paths "for compatibility" in case anyone depends on it, despite their being not only duplicative but literally unreachable. Another searing indictment on the incompetence of the countless professional programmers whose work served as training data.

GPT-5 + Codex is so fast that when I expressed suspicion that a script was returning too few results (via | wc -l
), Codex corrected me that I should have passed --count
instead. Sure enough, that worked.
Checked git status and realized Codex implemented the --count
flag in the script concurrently as it corrected me for not having used it! Gaslit by a robot!
You won't believe this Codex fork
Reddit turned me onto this just-every/code fork of OpenAI's Codex CLI last night. Since it uses the binary name coder
to differentiate it from code
and codex
, I guess we should just call this thing Coder.
In addition to everything you get with Codex:
- A built-in diff viewer (Ctrl+D). If you're like me, you often have Claude Code or Codex open in one window and your preferred Git UI (I use Fork) in a second window, so having it integrated is wonderful. Moreover, while viewing a diff, you can press
e
to request an explanation of the specific change you're looking at - Built-in browser support with ASCII previews (Ctrl+B) Like Playwright, it automates browsers over the very fast CDP, and it takes zero futzing to get started with it (unlike tacking an MCP tool onto Claude)
- Multi-agent consensus If you're a real sicko, you can hand the same question or task to all three of GPT, Claude, and Gemini and keep the consensus winner among them
Coder also has a themeable, more stable curses-like UI (as opposed to top-level terminal scrollback). It's the rare case of a community taking a heavily-funded corporate open source project and adding a lot of visual flair and spit polish to it. But I'll be damned if this isn't a much nicer experience than either Claude Code or Codex out of the box. (I can't speak to OpenCode, but since it doesn't support ChatGPT subscriptions, I'm not interested in it.)
From my friend Anthony Salamon on what it's like to win an award in the film industry:
The psychology of winning is also more complex than anyone admits. There's an immediate high, followed by a weird emptiness. You've achieved something you've been working toward, and suddenly you need new goals. Some winners describe a mild depression that follows major recognition, the "What now?" syndrome that comes when you realize that achieving your dreams doesn't fundamentally change who you are or solve your deeper creative challenges.
In my experience this phenomenon extends to any achievement or event that redounds to a significant moment of extrinsic validation in one's career. Anthony's description of the hangover effect following an award win mirrors exactly what it felt like to give a big conference talk or keynote address, especially if it was a "first" for me and doubly-so if it was very well received. It was always followed by a brief and intense emotional high followed by a deep well of uncertainty and exhaustion. A lesson I learned every conference season and promptly unlearned just in time for the subsequent year's conference season.
Speaking of conference season, I retired from speaking a year ago and it's that time again. Rails World is this week and lots of my friends are in Amsterdam and I am experiencing absolutely no FOMO. Don't miss it at all. Few things are as intrinsically validating as accurately forecasting how a consequential decision will ultimately make you feel. Zero regrets.
An insightful take from Hugo Tunius that makes a distinction between sideloading apps and controlling what software runs on "hardware you own":
When Google restricts your ability to install certain applications they aren't constraining what you can do with the hardware you own, they are constraining what you can do using the software they provide with said hardware.
To wit, if you own your iPhone outright, it's completely reasonable to demand that you be able to boot an alternative operating system and, to whatever extent regulatory action against the platform holders is warranted, it should be targeting this layer of the software stack as opposed to mandating how specific features of the operating system ought to function.
Which means the remedy would look a bit like the surprisingly-successful Right to Repair movement:
However, our critique shouldn't be of the restrictions in place in the operating systems they provide – rather, it should focus on the ability to truly run any code we want on hardware we own. In this context this would mean having the ability and documentation to build or install alternative operating systems on this hardware. It should be possible to run Android on an iPhone and manufacturers should be required by law to provide enough technical support and documentation to make the development of new operating systems possible.
A "Right to Run" movement that demanded hardware vendors enable their devices to run unsigned operating systems—and to perhaps provide documentation and device drivers—seems to me like it would stand on far firmer legal and conceptual ground than the knots the Europe Commission has tied itself in trying to enforce the Digital Markets Act.
Of course, pragmatist regulators would point out that approximately abso-fucking-nobody would go to the trouble of running "Linux on the phone," because of how miserable an experience it would be relative to using the default operating system. And the platform holders would justifiably cry foul that criminals and state actors could effectively rootkit people's devices and gain an unbelievable amount of surveillance and control over their lives. But there's no form of freedom that doesn't pose these sorts of risks.
Anyway, interesting idea.
Video of this episode is up on YouTube:
Remember it is your civic duty to e-mail me at podcast@searls.co. As of this episode, that address is monitored by Fastmail, so there's a higher probability I'll actually get your e-mail!
Some links you won't click:
Cinder block developers
What Scott means when he says he's a cinder block developer. Clipped from Hotfix v42.0.1.
Oh, and here's that episode of The Secret Life of Machines on washing machines

I don't know who needs to hear this, but despite being bare bones from a feature-set perspective, Codex CLI with GPT-5 is much, much better at some coding ecosystems than Claude Code with Opus 4.1/Sonnet.
Codex writes competent Swift that does what I ask, nothing more. Claude hallucinates code all day.
This blog has a comment system
The day before we recorded our episode of Hotfix, Scott Werner asked a fair question: "so, if you're off social media and your blog doesn't have a comment system, how do you want people to respond to your posts? Just email?"
I answered, "actually my blog does have a comment system."
Here's how to leave a comment on this web site:
- Read a post
- Think, "I want to comment on this"
- Draft a post on your blog
- Add a hyperlink to my post
- Paste an excerpt to which you want to respond
- Write your comment
- Hit publish
I admit, it's quaint. It involves a number of invisible steps, like 2.1
where you start a blog (which is actually pretty easy but not free of friction). You should try it.
It is 2025 and the Web—the capital-W Web—is beleaguered. The major platforms have long-since succumbed to enshittification, but their users aren't going anywhere. Some among us courageously voice their dissent, but always from the safe confines of their favorite walled garden. They drop a note in the jailkeeper's suggestion box as they scroll past the Squarespace ads littering their algorithmic timelines. Others have fled to open and open-flavored networks, but everyone eventually realizes they can't go home again.
But that's not why I want you to adopt this blog's commenting system. I'm not a high-minded individual who cares about the intellectual project of the World Wide Web as a bastion for free expression or whatever the fuck. No. I just had a super rad time on the Internet from 2000 to 2006 and I want to do my part to bring it back.
Back then, I would find a blog and follow it—via its feed when possible, or else by adding it to a folder of bookmarks—and check it daily.
But what about discoverability? How did anyone find these websites? Bloggers couldn't rely on platforms' social graphs or algorithmic timelines to build awareness, so they had to bake discoverability into the product. Some sites had a "blogroll" of recommendations in the sidebar. But the most effective approach was the art of "blogging as a conversation." When an author read something that provoked them to write, they'd link to the offending piece, excerpt it, and provide their own commentary. And because humans are vain, the original post's author would frequently circle back and write their own response to the response. The net effect was that each author's audience would get exposure to the other writer. Even if the authors were in violent disagreement, readers of one might appreciate the other's perspective and subscribe to them.
Blogging as a conversation—as a comments section—was valuable because it was purely positive-sum. As an author, I benefit because another author's opinions inspired me to write. The other author benefits because linking to them offers access to my readership. My readers benefit because they're exposed to complementary and contrasting viewpoints.
Growth was slow and organic but more meaningful and durable. It was a special time.
More on my personal history with blogging
If I really enjoyed someone's blog, I'd rush to read their stuff first. If an author's posts weren't so stimulating, I wasn't shy about unsubscribing. And I could afford to be picky—there was no shortage of content! Even with aggressive curation, by 2005 I had subscribed to so many feeds in Google Reader that I struggled to stay on top of them all. My grades suffered because I was "j-walking" hundreds of blog posts each day instead of doing homework.
Then, Facebook's feed, Tumblr, and Twttr came along, and they took the most enjoyable parts of surfing the 1.0 Web—novel information and connectivity with others—and supercharged them. They were "good Web citizens" in the same way the closed-source, distributed-to-exactly-one-server Bluesky is today. The timelines were reverse chronological. They handled the nerdy tech stuff for you. None of the feeds had ads yet.
Blogging didn't stand a chance.
I failed to see it at the time, but blogging did have one advantage over the platforms: it was a goddamn pain in the ass. Whether you flung files over an FTP client or used a CMS, writing a blog post was an ordeal. If you were going to the trouble of posting to your blog, you might as well put your back into it and write something thoughtful. Something you could take pride in. Something with a permalink that (probably wouldn't, but) could be cited years later.
The platforms offered none of that. You got a tiny-ass text area, a stingy character limit, and a flood of ephemera to compete with. By demoting writing to a subordinate widget of the reading interface, the priority was clear: words were mere grist for the content mill. The shame of it all was that these short-form, transient, low-effort posts nevertheless sated many people's itch to write at all. I was as guilty of this as anyone. From 2009 through 2020, I devoted all my writing energy to Twitter. Except for that brief year or two where Medium was good, I basically stopped thinking in longform. Instead, I prided myself on an ability to distill 2,000-word essays down to 140-character hot takes. Many of those takes reached millions of people and made me feel good for a very brief amount of time.
My brain was cooked. When it finally sank in, I quit.
It took almost three years to recover. I'm on the other side now, and am happy to report I can now think thoughts more than a sentence or two long.
Last night, I got dinner with two old friends, Chris Nelson and Joshua Wood. Josh asked how it's been since I quit paying attention to social media. I thought about the unfinished draft of this post.
In truth, this blog and its attendant podcast empire have been a refuge for my psyche. A delightful place to share pieces of myself online. Somewhere to experiment in both form and format. A means of reclaiming my identity from a smattering of social media profile pages and into something authentic and unique.
Today, as the platforms wane, it feels like this conversational approach to blogging is seeing new life. As a readership has slowly gathered around this blog, I've separately been curating a fresh list of thoughtful bloggers that inspire me to write. Maybe I'll add a blogroll to my next redesign. I'm already writing more linkposts.
In short, blogging might be back. Hell, I just came back from coffee with my friend Anthony, and—without my having brought up the topic—he showed me his new blog.
So, if you're considering engaging with my comment system—if you're thinking about starting a blog or dusting off your old one—here's some unsolicited advice:
- Do it for you. Priority one is taking the time to grapple with your thoughts, organize your ideas, and put them into words. Priority two is reaching the finish line and feeling the pride of authorship. That anyone actually reads your work should be a distant third place
- Focus on building an audience rather than maximizing reach. Getting in front of eyeballs is easier on the platform, but it's fleeting. Platforms reward incitement, readers reward insight. Success is a lagging indicator of months and years of effort, but it's long-lasting. I genuinely believe each of the readers of this site are as valuable as a hundred followers on social media
- Give your blog your best work. Don't waste your creative juices trying to be clever on someone else's app. Consider syndicating crossposts to your social accounts as a breadcrumb trail leading back to your homepage. You can do this with Buffer, Publer, SocialBee, or my upcoming POSSE Party
- Cut yourself some slack. Pretty much everyone is an awful writer. If you saw how long it takes me to write anything of substance, you'd agree that I'm an awful writer, too. Thankfully, good ideas have a way of shining through weak rhetoric and bad grammar. All that matters is training this learned response: have an idea, write it down, put it out
That's all I've got. If you choose to leave a comment on this post on your own blog, e-mail it to me, and I'd be delighted to read it. Maybe it'll inspire me to write a response! 💜

For somebody who hates Apple so much, you'd think Tim Sweeney would be above resorting to the "you're holding it wrong" defense. videogameschronicle.com/news/unreal-engine-5-performance-issues-are-mainly-due-to-devs-not-optimising-properly-epic-ceo-tim-sweeney-says/
AI is exposing order-takers
One thing few people are talking about is how it's not as simple as people being sticks in the mud with respect to the adoption of AI tools, it's that once they get their hands on this tremendously capable set of tools, they lack the imagination to find any use for it. "They've been given this rocket ship and they've got no fucking clue where to fly it."
Clipped from the back half of my discussion with Scott Werner on Hotfix.
Video of this episode is up on YouTube:
🔥Hotfix🔥 is back with a new guest! Scott Werner is the CEO of Sublayer, helps organize the Artificial Ruby meetup in NYC, and is the author of the extremely well-named (and well-written) Substack, Works on my Machine.
In this conversation, we jointly grapple with WTF is happening to programming as a career. Did the unprecedented peacetime the software industry experienced from 2005-2022 make us all soft? Is the era of code-writing agents fundamentally changing the nature of the job? Should we be less like DHH/Matz and more like why the lucky stiff?
We'd love to get your feedback to podcast@searls.co — I'll read it all and flag relevant questions and comments for the next Breaking Change.
You can follow Scott Werner online at:
A handful of things we mentioned:
- There's no AI in Team
- The Goal book
- Lean Thinking vs Fat Thinking (a newsletter that draws a different lesson from The Goal)
- why the lucky stiff (aka "_why")
- That fireside chat between Matz and DHH
- Sublayer's product, APM (Actions Per Minute)