Skip to main content

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

GroupRequired (default)RollbackToken scope
Managerfalsere-clone tap, git revert HEAD --no-edit, pushGITHUB_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-tap

Full 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-app

Authentication

VariableDescription
GITHUB_TOKENToken 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_image or archive kind) 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, set update_existing_pr: true to 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

FieldTypeDefaultDescription
namestringproject nameCask name
repositoryobjectrequiredTap repository (owner, name)
directorystringCasksDirectory in the tap repo
descriptionstringCargo [package].descriptionCask description. Derived from Cargo.toml; set to override.
homepagestringCargo [package].homepageHomepage URL. Derived from Cargo.toml; set to override.
licensestringnoneLicense identifier
appstringnoneApplication name for app stanza
binarieslistnoneBinaries to symlink
manpageslistnoneMan pages to install
caveatsstringnonePost-install caveats message
servicestringnoneService definition
custom_blockstringnoneRaw Ruby inserted into the cask
livecheckobjectskiplivecheck do … end stanza for version polling. See Livecheck.
alternative_nameslistnoneAlternative cask names
idslistnoneFilter by build IDs
skip_uploadstring/boolnoneSkip git push ("auto" skips for prereleases)
commit_authorobjectnoneGit commit author (name, email)
commit_msg_templatestringauto-generatedCustom commit message (template)

URL config (url)

FieldTypeDefaultDescription
templatestringauto-derivedDownload URL template
verifiedstringnoneVerified domain for verified: stanza
usingstringnoneDownload strategy (e.g., :homebrew_curl)
cookiesmapnoneHTTP cookies for the download
refererstringnoneReferer header
headerslistnoneCustom HTTP headers
user_agentstringnoneCustom user agent string
datamapnonePOST data for form submissions

Completions (completions)

FieldTypeDescription
bashstringPath to bash completion file
zshstringPath to zsh completion file
fishstringPath to fish completion file

Uninstall / Zap (uninstall, zap)

FieldTypeDescription
launchctllistLaunch agent/daemon identifiers to stop
quitlistApplication bundle IDs to quit
login_itemlistLogin item names to remove
deletelistFile paths to delete
trashlistFile 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)

FieldTypeDescription
executablestringBinary to generate completions from
argslistArguments to pass to the executable
base_namestringBase name for completion files
shell_parameter_formatstringCompletion framework type (arg, clap, cobra, etc.)
shellslistTarget shells (bash, zsh, fish, pwsh)

Dependencies (dependencies)

Each entry can specify either cask or formula:

dependencies:
  - formula: cmake
  - cask: xquartz

Conflicts (conflicts)

Each entry can specify either cask or formula:

conflicts:
  - cask: another-app

Livecheck (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
FieldTypeDefaultDescription
strategystringnoneLivecheck strategy symbol (e.g. github_latest, git, page_match)
urlstringurl:url / :homepage symbol shorthand, or a literal URL string
regexstringnoneRaw Ruby regex (e.g. %r{v(\d+\.\d+)}i) for page_match-style strategies
skipboolautotrue forces the skip stanza; defaults to skip when no strategy/url/regex is set
skip_reasonstringAuto-generated on release.Custom message for the skip stanza

url accepts a Ruby symbol shorthand (url / stable / head / homepageurl :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_arm blocks (the shape above). The same mechanism handles Linux casks: a darwin/amd64 + darwin/arm64 + linux/amd64 release produces an on_macos block (with two arch entries) and an on_linux block.

  • One macOS architecture → a flat top-level url / sha256, with no on_intel / on_arm wrappers:

    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-arch amd64 and arm64 artifacts remain in the catalog alongside the universal binary, so the cask renders the full on_intel + on_arm multi-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 flat url / sha256 shape, 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_image or archive kind)
  • Requires SHA256 checksum metadata on the artifact
  • Emits per-arch on_intel / on_arm blocks for multi-arch macOS releases; a single macOS arch renders a flat url / 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"