The tap and the formula
You just finished building a Go CLI tool. It compiles to a single static binary, parses flags cleanly, and does exactly what you need. Now you want your teammates or the wider community to install it with a single command. brew install mycli sounds perfect. Homebrew does not host third-party Go tools in its core repository. You need to create a custom tap and write a formula that tells the package manager where to find your binary and how to place it on the system.
Think of a Homebrew tap as a personal app store. The core Homebrew repository is the official store maintained by the community. Your tap is a curated shelf you control. The formula is the instruction manual for that shelf. It tells the installer where to download the package, how to verify it has not been tampered with, and exactly which folder to drop the executable into. Go makes this straightforward because the language compiles to a single binary. You do not need to bundle a runtime or manage complex dependency trees. You just need to point Homebrew at a tarball or a Git repository.
How the formula actually works
Here is the simplest formula for a pre-compiled Go binary. It downloads a tarball, verifies the checksum, and moves the executable into the Homebrew bin directory.
class Mycli < Formula
# Metadata helps users discover the tool in `brew search`
desc "A fast CLI tool for processing JSON data"
homepage "https://github.com/youruser/mycli"
# Point to the release asset. Homebrew replaces version placeholders automatically.
url "https://github.com/youruser/mycli/releases/download/v1.0.0/mycli-1.0.0.tar.gz"
# Cryptographic fingerprint prevents supply-chain tampering
sha256 "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2"
def install
# Extract the archive and move the binary to the system PATH
bin.install "mycli"
end
test do
# Run a quick smoke test to verify the binary executes correctly
system bin/"mycli", "--version"
end
end
Homebrew reads this file from top to bottom. The desc and homepage fields populate the help text. The url field tells curl or wget where to fetch the package. The sha256 field is mandatory. Homebrew computes the hash of the downloaded file and compares it against your string. If they differ, the installation aborts immediately. The def install block runs after extraction. bin.install copies your executable into $(brew --prefix)/bin, which is already on the user's PATH. The test do block runs automatically when you run brew test mycli. It catches broken binaries before users complain.
Formulas are Ruby classes that inherit from Formula. You do not need to be a Ruby developer to write them. Homebrew provides a DSL that abstracts away the filesystem paths and environment variables. The convention is to name the file after the tool, using lowercase with hyphens replaced by underscores. mycli.rb becomes the formula for mycli.
Homebrew formulas are contracts. Keep them minimal and let the package manager handle the heavy lifting.
Handling real releases
Production CLIs ship to multiple architectures. Apple Silicon Macs run arm64. Older Intel Macs run x86_64. Homebrew expects you to handle both. You can bundle two binaries in one tarball, or you can use Homebrew's architecture detection to point to separate release assets. The latter is cleaner and saves users from downloading unnecessary files.
Here is a realistic formula that handles architecture routing and includes a proper test suite.
class Mycli < Formula
desc "A fast CLI tool for processing JSON data"
homepage "https://github.com/youruser/mycli"
# Use on_macos to serve architecture-specific binaries
on_macos do
on_arm do
url "https://github.com/youruser/mycli/releases/download/v1.0.0/mycli-1.0.0-darwin-arm64.tar.gz"
sha256 "arm64hashgoeshere"
end
on_intel do
url "https://github.com/youruser/mycli/releases/download/v1.0.0/mycli-1.0.0-darwin-amd64.tar.gz"
sha256 "amd64hashgoeshere"
end
end
def install
# Go binaries are statically linked. No extra libraries needed.
bin.install "mycli"
end
test do
# Verify the binary runs and returns exit code 0
system bin/"mycli", "--version"
# Test a core feature to catch regressions
(testpath/"input.json").write({ "status" => "ok" }.to_json)
system bin/"mycli", "parse", testpath/"input.json"
end
end
The on_macos, on_arm, and on_intel blocks let Homebrew pick the right URL at install time. The test do block now validates actual functionality, not just version output. testpath provides a temporary sandbox directory. to_json is a Ruby helper that serializes the hash. This test runs in isolation and cleans up after itself.
You will need a CI pipeline to keep this tap updated. When you cut a new release on GitHub, a workflow should generate the sha256 for each binary, update the formula file, commit it to your tap repository, and push the changes. Manual updates break quickly. Automate the tap refresh.
Distribution is a pipeline, not a one-time script. Automate the checksums and the formula updates.
Where things break
Homebrew is strict about reproducibility. The most common failure is a checksum mismatch. If you upload a new binary but forget to update the sha256 string, the installer halts with Checksum mismatch for formula mycli. This is a safety feature. It prevents corrupted downloads or malicious replacements. Always regenerate the hash after every release. Use shasum -a 256 mycli.tar.gz on macOS or sha256sum mycli.tar.gz on Linux.
Another frequent issue is missing executables in the tarball. If your archive contains a nested directory like mycli-1.0.0/bin/mycli, but the formula expects mycli at the root, Homebrew throws No such file or directory during the bin.install step. Flatten your release archives. The binary should sit at the top level of the extracted folder.
Testing failures happen when the formula assumes a dependency that is not available in the sandbox. The test do block runs in a clean environment. It does not inherit your local PATH or installed packages. If your CLI requires jq or git, you must declare them in the formula with depends_on "jq" or install them inside the test block. Undeclared dependencies cause silent failures or confusing error traces.
Architecture mismatches also surface during testing. If you run brew test on an Intel Mac but the formula only defines an on_arm block, Homebrew cannot resolve the URL. The build fails with No URL defined for this architecture. Always define fallback blocks or test on both machine types.
Homebrew rejects ambiguity. Define your paths, verify your hashes, and test in a clean environment.
Choosing a distribution path
Use a pre-compiled binary tap when your Go CLI compiles to a single executable and you want users to install it in seconds. Use a source-build formula when your tool depends on C libraries or requires platform-specific compilation flags. Use a custom tap when you control the release cycle and want to maintain version history alongside your code. Skip Homebrew when your tool is internal-only or when your team already uses a containerized deployment strategy.
Distribution matches your release rhythm. Pick the path that scales with your workflow.