Article illustration 1

For developers building command-line tools, Homebrew offers a streamlined installation experience users love. Yet many creators default to language-specific package managers like npm or RubyGems, intimidated by Homebrew's perceived complexity. As Justin Searls candidly admits: "I had no fucking clue how Homebrew works... I'm not enough of a neckbeard to peer behind the curtain."

But distributing via Homebrew doesn't require deep systems expertise—just clear guidance. Here's how to package your CLI tool for Homebrew's ecosystem, complete with automation for painless updates.

Decoding Homebrew's Terminology

Before diving in, translate these essential terms:
- Formula: Package definition (your CLI's blueprint)
- Tap: Git repository hosting formulae
- Bottle: Pre-built binary package
- Cellar: Installation directory (/opt/homebrew/Cellar)
- Keg: Version-specific installation directory

The Publishing Workflow: 3 Critical Steps

1. Create Your Tap

Homebrew prefers community tools live in personal taps rather than core. Initialize your repository:

brew tap-new your_github_handle/homebrew-tap
git -C $(brew --repo your_github_handle/tap) remote add origin [email protected]:your_github_handle/homebrew-tap.git

Push the scaffolded repository to GitHub. Users can now install tools via:

brew tap your_github_handle/tap

2. Build Your Formula

Reference versioned tarballs for reproducibility. After tagging a release in your CLI's repo:

git tag v1.0.0
git push --tags

Generate the formula:

brew create https://github.com/you/your_cli/archive/refs/tags/v1.0.0.tar.gz \
  --tap your_github_handle/homebrew-tap \
  --set-name your_cli \
  --ruby  # Or --python, --node, etc.

Critical Formula Tweaks:
- Replace outdated macOS Ruby with modern version:

depends_on "ruby@3"  # Instead of uses_from_macos "ruby"
  • Add livecheck for version tracking
  • Implement basic install test:
test do
  assert_match "Usage: ", shell_output("#{bin}/your_cli --help")
end

3. Automate Release Updates

Manually updating SHAs and versions is unsustainable. Implement this GitHub Actions workflow in your tap repository:

name: Update Formula

on:
  release:
    types: [published]

jobs:
  update:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout tap
        uses: actions/checkout@v4
        with:
          token: ${{ secrets.HOMEBREW_TAP_TOKEN }}

      - name: Update formula
        run: |
          brew update
          brew bump-formula-pr \
            --url=https://github.com/$GITHUB_REPOSITORY_OWNER/your_cli/archive/${{ github.ref }}.tar.gz \
            --sha256=$(curl -sL https://github.com/$GITHUB_REPOSITORY_OWNER/your_cli/archive/${{ github.ref }}.tar.gz | shasum -a 256 | cut -d' ' -f1) \
            --no-browse \
            --write \
            --verbose \
            your_cli

      - name: Commit changes
        run: |
          git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
          git config user.name "github-actions[bot]"
          git commit -am "Update your_cli to ${{ github.ref_name }}"
          git push

Prerequisites:
- Create HOMEBREW_TAP_TOKEN secret with repo write permissions
- Replace your_cli with actual formula name

Why This Matters

Packaging for Homebrew future-proofs your tool. As Searls notes: "I could theoretically swap out the implementation for some other language entirely without disrupting users' ability to upgrade." The initial setup delivers compounding returns—subsequent tools become trivial to publish.

While the process feels arcane initially, mastering it unlocks professional-grade distribution. Developers installing your tool now enjoy the same seamless experience they get from household-name CLIs:

brew tap your_github_handle/tap
brew install your_cli

And that’s worth raising a glass to. 🍻

Source: Justin Searls