Homebrew Casks
Generate Homebrew cask formulae for macOS applications
Anodizer generates Homebrew Cask .rb files for your macOS artifacts and pushes them to your tap repository. A cask is the canonical Homebrew channel for pre-compiled binaries — both GUI .app bundles (via the app stanza) and CLI tools (via a binary stanza that symlinks an executable onto the user's PATH). The Homebrew Formula path — which compiles from source — is deprecated upstream for pre-built release binaries; use a cask. Set binaries: to ship a CLI; set app: to ship a GUI application; set both when a bundle also exposes a CLI.
Classification
| Group | Required (default) | Rollback | Token scope |
|---|---|---|---|
| Manager | false | re-clone tap, git revert HEAD --no-edit, push | GITHUB_TOKEN contents:write |
See Release resilience for the full classification table and the Submitter gate semantics.
The required: field
Default: false — a Homebrew Cask push failure is logged but does not fail the release.
Set required: true to make the release exit non-zero if this publisher fails:
homebrew_casks:
- name: myapp
repository:
owner: myorg
name: homebrew-tap
required: true
See Publish overview — the required: field for the full semantics.
Minimal config
homebrew_casks:
- name: myapp
repository:
owner: myorg
name: homebrew-tapFull config reference
homebrew_casks:
- name: myapp # optional; cask name (default: project name)
repository:
owner: myorg # required
name: homebrew-tap # required
token: "{{ Env.GITHUB_TOKEN }}" # optional; falls back to GITHUB_TOKEN env
branch: main # optional; target branch
pull_request:
enabled: false # optional; open PR instead of direct push
base: main
directory: Casks # optional; directory in the tap repo
description: "" # optional
homepage: "" # optional
license: "" # optional
app: "" # optional; app stanza name
binaries: [] # optional; binaries to symlink
manpages: [] # optional
caveats: "" # optional; post-install message
service: "" # optional; service definition
custom_block: "" # optional; raw Ruby inserted into cask
livecheck: # optional; version polling (default: skip)
strategy: github_latest # optional; livecheck strategy symbol
url: url # optional; :url shorthand or a literal URL
regex: "" # optional; raw Ruby for page_match strategies
skip: false # optional; true forces the skip stanza
skip_reason: "" # optional; custom skip message
alternative_names: [] # optional
ids: [] # optional; filter by build IDs
skip_upload: false # optional; "auto" skips prereleases
commit_author:
name: ""
email: ""
commit_msg_template: "" # optional
url:
template: "" # optional; download URL template
verified: "" # optional
using: "" # optional; download strategy
completions:
bash: "" # optional; path to bash completion
zsh: "" # optional
fish: "" # optional
uninstall:
quit: []
delete: []
launchctl: []
zap:
trash: []
hooks:
pre:
install: ""
post:
install: ""
dependencies: # optional
- formula: cmake
- cask: xquartz
conflicts: # optional
- cask: another-appAuthentication
| Variable | Description |
|---|---|
GITHUB_TOKEN | Token with push access to your tap repository |
The token can also be set via repository.token in the config.
Common gotchas
- macOS only: only macOS artifacts (
disk_imageorarchivekind) are included. Linux/Windows targets are ignored. - SHA256 required: cask files require a checksum on every artifact. Ensure the checksum stage runs before the cask publisher.
update_existing_pr: if your tap uses a PR-based workflow, setupdate_existing_pr: trueto force-push to an existing open branch rather than opening a duplicate PR. See Cask existing PR behavior in the Homebrew doc.
Republish / update behavior
Cask files are updated in-place on each release; no recovery flag is required for re-cuts. When a PR-based workflow is configured and a prior run left an open PR, set update_existing_pr: true to force-push the updated cask file instead of opening a duplicate. See Cask existing PR behavior in the Homebrew doc and Recovery flags for the full mechanism.
Homebrew cask config fields
| Field | Type | Default | Description |
|---|---|---|---|
name | string | project name | Cask name |
repository | object | required | Tap repository (owner, name) |
directory | string | Casks | Directory in the tap repo |
description | string | Cargo [package].description | Cask description. Derived from Cargo.toml; set to override. |
homepage | string | Cargo [package].homepage | Homepage URL. Derived from Cargo.toml; set to override. |
license | string | none | License identifier |
app | string | none | Application name for app stanza |
binaries | list | none | Binaries to symlink |
manpages | list | none | Man pages to install |
caveats | string | none | Post-install caveats message |
service | string | none | Service definition |
custom_block | string | none | Raw Ruby inserted into the cask |
livecheck | object | skip | livecheck do … end stanza for version polling. See Livecheck. |
alternative_names | list | none | Alternative cask names |
ids | list | none | Filter by build IDs |
skip_upload | string/bool | none | Skip git push ("auto" skips for prereleases) |
commit_author | object | none | Git commit author (name, email) |
commit_msg_template | string | auto-generated | Custom commit message (template) |
URL config (url)
| Field | Type | Default | Description |
|---|---|---|---|
template | string | auto-derived | Download URL template |
verified | string | none | Verified domain for verified: stanza |
using | string | none | Download strategy (e.g., :homebrew_curl) |
cookies | map | none | HTTP cookies for the download |
referer | string | none | Referer header |
headers | list | none | Custom HTTP headers |
user_agent | string | none | Custom user agent string |
data | map | none | POST data for form submissions |
Completions (completions)
| Field | Type | Description |
|---|---|---|
bash | string | Path to bash completion file |
zsh | string | Path to zsh completion file |
fish | string | Path to fish completion file |
Uninstall / Zap (uninstall, zap)
| Field | Type | Description |
|---|---|---|
launchctl | list | Launch agent/daemon identifiers to stop |
quit | list | Application bundle IDs to quit |
login_item | list | Login item names to remove |
delete | list | File paths to delete |
trash | list | File paths to trash (preserves app state) |
Hooks (hooks)
hooks:
pre:
install: "system_command '/usr/bin/some-setup'"
uninstall: "system_command '/usr/bin/some-cleanup'"
post:
install: "system_command '/usr/bin/post-setup'"
uninstall: "system_command '/usr/bin/post-cleanup'"Generated completions (generate_completions_from_executable)
| Field | Type | Description |
|---|---|---|
executable | string | Binary to generate completions from |
args | list | Arguments to pass to the executable |
base_name | string | Base name for completion files |
shell_parameter_format | string | Completion framework type (arg, clap, cobra, etc.) |
shells | list | Target shells (bash, zsh, fish, pwsh) |
Dependencies (dependencies)
Each entry can specify either cask or formula:
dependencies:
- formula: cmake
- cask: xquartzConflicts (conflicts)
Each entry can specify either cask or formula:
conflicts:
- cask: another-appLivecheck (livecheck)
By default the cask emits livecheck do skip "Auto-generated on release." end —
a binary cask's url/sha256 are rewritten on every release, so there is
nothing stable for brew livecheck to poll. Set a strategy (and optionally
url/regex) to opt into active version detection. For a GitHub-released
project, github_latest against the cask's own url stanza (:url, the
idiomatic cask shorthand) is the right pairing:
homebrew_casks:
- name: myapp
repository:
owner: myorg
name: homebrew-tap
livecheck:
strategy: github_latest
url: url
renders into the cask:
livecheck do
url :url
strategy :github_latest
end| Field | Type | Default | Description |
|---|---|---|---|
strategy | string | none | Livecheck strategy symbol (e.g. github_latest, git, page_match) |
url | string | url | :url / :homepage symbol shorthand, or a literal URL string |
regex | string | none | Raw Ruby regex (e.g. %r{v(\d+\.\d+)}i) for page_match-style strategies |
skip | bool | auto | true forces the skip stanza; defaults to skip when no strategy/url/regex is set |
skip_reason | string | Auto-generated on release. | Custom message for the skip stanza |
url accepts a Ruby symbol shorthand (url / stable / head / homepage →
url :url) or a literal URL string. Setting skip: false without any of
strategy/url/regex falls back to skip with a warning — an empty
livecheck do … end is invalid. anodizer's cask livecheck is fully
configurable (strategy, url, regex, skip), matching how the
overwhelming majority of real Homebrew casks declare version detection.
Multi-architecture casks
When a release builds more than one macOS architecture (typically
darwin/amd64 for Intel Macs plus darwin/arm64 for Apple Silicon), anodizer
emits a cask body with per-architecture on_intel / on_arm stanzas. Each
stanza carries its own url and sha256, so brew install serves every Mac
host the binary built for its CPU:
cask "myapp" do
version "1.2.3"
name "myapp"
livecheck do
skip "Auto-generated on release."
end
on_macos do
on_arm do
sha256 "2222222222222222222222222222222222222222222222222222222222222222"
url "https://github.com/myorg/myapp/releases/download/v#{version}/myapp-darwin-arm64.tar.gz"
end
on_intel do
sha256 "1111111111111111111111111111111111111111111111111111111111111111"
url "https://github.com/myorg/myapp/releases/download/v#{version}/myapp-darwin-amd64.tar.gz"
end
end
binary "myapp"
end
The version substring in each URL is rewritten to #{version} so Homebrew
auto-updates the download on the next release. The url/verified/using and
other URL config values you set apply inside every per-arch
block.
This emission is automatic — there is no flag to enable it. anodizer decides the shape from the artifacts present in the release:
-
Multiple macOS architectures → per-arch
on_intel/on_armblocks (the shape above). The same mechanism handles Linux casks: adarwin/amd64+darwin/arm64+linux/amd64release produces anon_macosblock (with two arch entries) and anon_linuxblock. -
One macOS architecture → a flat top-level
url/sha256, with noon_intel/on_armwrappers:cask "myapp" do version "1.2.3" sha256 "1111111111111111111111111111111111111111111111111111111111111111" url "https://github.com/myorg/myapp/releases/download/v#{version}/myapp-darwin-arm64.tar.gz" name "myapp" # ... binary "myapp" end
Each OS×arch slot is filled by the first artifact found in kind precedence
order: disk_image (.dmg) > archive (.tar.gz/.zip) > uploadable
binary. The first kind that supplies a given slot wins, so a release that
produces both a .dmg and a .tar.gz per arch fills the cask from the .dmg.
Dedup is per-OS, so a macOS intel entry never suppresses a Linux intel
entry.
Every artifact filling a slot must carry sha256 metadata: a cask block
with an empty sha256 "" line fails brew style and aborts brew install
(Homebrew verifies the digest before extracting), so a missing checksum is a
hard error rather than a degraded cask.
Interaction with universal_binaries
The per-arch slots are always filled from the real per-architecture macOS
artifacts (darwin/amd64 and darwin/arm64), never from a lipo'd universal
binary — a universal artifact has the synthetic darwin-universal target
(architecture all), which matches neither the intel nor the arm slot and
is skipped by the cask builder. What changes is whether the per-arch artifacts
still exist:
universal_binaries.replace: false(or unset) → the per-archamd64andarm64artifacts remain in the catalog alongside the universal binary, so the cask renders the fullon_intel+on_armmulti-arch body above.universal_binaries.replace: true→ the per-arch source artifacts are removed from the catalog once the universal binary is built. With both per-arch macOS slots gone, the cask falls back to the single-arch flaturl/sha256shape, served from whatever single non-universal macOS artifact remains.
So if you want a multi-arch cask with explicit on_intel / on_arm blocks,
keep replace: false; the universal binary then ships through other channels
while the cask serves each Mac its native slice.
Behavior
- Looks for macOS artifacts (
disk_imageorarchivekind) - Requires SHA256 checksum metadata on the artifact
- Emits per-arch
on_intel/on_armblocks for multi-arch macOS releases; a single macOS arch renders a flaturl/sha256 - Clones the tap repository, writes the cask file, commits, and pushes
- Default commit message:
"Brew cask update for {{ ProjectName }} version {{ Tag }}"
Full example
homebrew_casks:
- name: myapp
repository:
owner: myorg
name: homebrew-tap
directory: Casks
description: "My awesome application"
homepage: "https://example.com/myapp"
license: MIT
app: "MyApp.app"
uninstall:
quit:
- com.myorg.myapp
delete:
- "/Applications/MyApp.app"
zap:
trash:
- "~/Library/Preferences/com.myorg.myapp.plist"