---
title: "Homebrew Casks"
linkTitle: Homebrew
weight: 90
---

{{< g_version "v2.10" >}}

After releasing to GitHub, GitLab, or Gitea, GoReleaser can generate and publish
a _Homebrew Cask_ into a repository (_Tap_) that you have access to.

The `homebrew_casks` section specifies how the cask should be created.
You can check the
[Homebrew Cask documentation](https://docs.brew.sh/Cask-Cookbook),
for more details.

```yaml {filename=".goreleaser.yaml"}
homebrew_casks:
  -
    # Name of the cask
    #
    # Default: the project name.
    # Templates: allowed.
    name: myproject

    # Alternative names for the current cask.
    #
    # Useful if you want to publish a versioned cask as well, so users can
    # more easily downgrade.
    #
    # This feature is only available in GoReleaser Pro.
    # Templates: allowed.
    alternative_names:
      - myproject@{{ .Version }}
      - myproject@{{ .Major }}

    # IDs of the archives to use.
    # Empty means all IDs.
    ids:
      - foo
      - bar

    # Binary name inside the cask
    #
    # Default: the cask name.
    # Templates: allowed.
    binaries:
      - myapp
      - myapp2

    # App to use instead of the binary.
    # This will then make GoReleaser use only the DMG files instead of archives.
    #
    # {{< g_inline_pro >}}
    # Templates: allowed.
    app: Foo.app

    # Path to the manpage files.
    #
    # Templates: allowed.
    manpages:
      - man/myapp.1
      - man/myapp-subcmd.1

    # Completions for different shells
    #
    # Templates: allowed.
    completions:
      bash: completions/myapp.bash
      zsh: completions/myapp.zsh
      fish: completions/myapp.fish

    # Generate shell completions by running the binary at install time.
    #
    # All fields except `executable` are optional.
    #
    # {{< g_inline_version "v2.15" >}}
    generate_completions_from_executable:
      # Path to the executable inside the cask bundle.
      #
      # Default: the first binary in the cask.
      executable: "bin/myapp"

      # Additional subcommand arguments passed to the executable.
      args:
        - completions

      # Override the base name used for the completion files.
      # Default: the binary name.
      base_name: "myapp"

      # How the shell name is passed to the executable.
      # Known values (rendered as Ruby symbols): arg, clap, click, cobra, flag, none, typer.
      # Any other string is treated as a custom prefix (e.g. "--complete-").
      shell_parameter_format: cobra

      # Shells to generate completions for.
      # Default: [bash, zsh, fish]. cobra and typer also include pwsh by default.
      shells:
        - bash
        - zsh
        - fish
        - pwsh

    # This information will be used to build the URL section of your Cask.
    #
    # You can set the template, as well as additional parameters.
    # These parameters can be used to provide extra headers, cookies, or other
    # download requirements for your application.
    # See https://docs.brew.sh/Cask-Cookbook#additional-url-parameters for more details.
    #
    # All fields are optional.
    url:
      # URL which is determined by the given Token (github, gitlab or gitea).
      #
      # Default depends on the client.
      # Templates: allowed.
      template: "https://github.mycompany.com/foo/bar/releases/download/{{ .Tag }}/{{ .ArtifactName }}"

      # Used when the domains of `url` and `homepage` differ.
      # Templates: allowed.
      verified: "github.com/owner/repo/"

      # Download strategy or format specification
      # See official Cask Cookbook for allowed values.
      # Templates: allowed.
      using: ":homebrew_curl"

      # HTTP cookies to send with the download request
      # Templates: allowed.
      cookies:
        license: "accept-backup"

      # HTTP referer header
      # Templates: allowed.
      referer: "https://example.com/download-page"

      # Additional HTTP headers
      # Templates: allowed.
      headers:
        - "X-Version: {{ .Version }}"

      # Custom User-Agent header
      # Templates: allowed.
      user_agent: "MyApp/1.0 (macOS)"

      # Custom body when using POST request
      # Templates: allowed.
      data:
        format: "dmg"
        platform: "mac"

    # The project name and current git tag are used in the format string.
    #
    # Templates: allowed.
    commit_msg_template: "Brew cask update for {{ .ProjectName }} version {{ .Tag }}"

    # Directory inside the repository to put the cask.
    # Default: Casks
    directory: Casks

    # Caveats for the user of your binary.
    caveats: "How to use this binary"

    # Your app's homepage.
    #
    # Default: inferred from global metadata.
    homepage: "https://example.com/"

    # Your app's description.
    #
    # Templates: allowed.
    # Default: inferred from global metadata.
    description: "Software to create fast and easy drum rolls."

    # Setting this will prevent goreleaser to actually try to commit the updated
    # cask - instead, the cask file will be stored on the dist directory
    # only, leaving the responsibility of publishing it to the user.
    # If set to auto, the release will not be uploaded to the homebrew tap
    # in case there is an indicator for prerelease in the tag e.g. v1.0.0-rc1
    #
    # Templates: allowed.
    skip_upload: true

    # Custom block for brew.
    # Can be used to specify alternate downloads for devel or head releases.
    #
    # This block is placed at the top of the cask definition.
    # It allows you to define custom modules and helper methods
    # for advanced tasks, such as dynamic URL construction.
    # For more information, see: https://docs.brew.sh/Cask-Cookbook#arbitrary-ruby-methods
    custom_block: |
      head "https://github.com/some/package.git"
      ...

    # Dependencies for the cask.
    dependencies:
      - cask: some-cask
      - formula: some-formula

    # Packages that conflict with your cask.
    conflicts:
      - cask: some-cask

    # Hooks for the cask lifecycle.
    #
    # Templates: allowed. {{< g_inline_version "v2.13" >}}
    hooks:
      pre:
        install: |
          system_command "/usr/bin/defaults", args: ["write", "com.example.app", "key", "value"]
        uninstall: |
          system_command "/usr/bin/defaults", args: ["delete", "com.example.app"]
      post:
        install: |
          system_command "/usr/bin/open", args: ["#{appdir}/MyApp.app"]
        uninstall: |
          system_command "/usr/bin/rm", args: ["-rf", "~/.myapp"]

    # Relative path to a Service that should be moved into the
    # ~/Library/Services folder on installation.
    service: "myapp.service"

    # Additional procedures for a more complete uninstall, including user files
    # and shared resources.
    zap:
      launchctl:
        - "my.fancy.package.service"
      quit:
        - "my.fancy.package"
      login_item:
        - "my.fancy.package"
      trash:
        - "~/.foo/bar"
        - "~/otherfile"
      delete:
        - "~/.foo/bar"
        - "~/otherfile"

    # Procedures to uninstall a cask.
    # Optional unless a pkg or installer artifact stanza is used.
    uninstall:
      launchctl:
        - "my.fancy.package.service"
      quit:
        - "my.fancy.package"
      login_item:
        - "my.fancy.package"
      trash:
        - "~/.foo/bar"
        - "~/otherfile"
      delete:
        - "~/.foo/bar"
        - "~/otherfile"

{{% g_include file="includes/repository.md" %}}
```

{{< g_templates >}}

## Signing and Notarizing

Casks are supposed to be signed, even if they are coming from a tap.

GoReleaser can [sign and notarize both binaries and apps](/customization/sign/notarize/), but,
Apple charges a yearly fee for that.

If you don't want to do it, you still have the option to tell macOS to remove
the quarantine bit from the binary on a post install hook:

```yaml {filename=".goreleaser.yaml"}
homebrew_casks:
  - name: foo
    hooks:
      post:
        # replace foo with the actual binary name
        install: |
          if OS.mac?
            system_command "/usr/bin/xattr", args: ["-dr", "com.apple.quarantine", "#{staged_path}/foo"]
          end
```

> [!CAUTION]
> **What happens if I don't follow the steps above?**
>
> **Not following this might lead to your app/binary to not run.**
>
> In these cases, users will see the infamous "_App Name is damaged and
> cannot be opened_" alert.
>
> If you don't want to do any of the steps above, you may want to instruct
> your users to run the appropriate `xattr` command manually.
>
> You may do so in using the `caveats` property, for example.

> [!WARNING]
> **xattr bypasses macOS security - use with caution**
>
> Use of `xattr` to bypass Gatekeeper circumvents macOS security protections
> designed to verify software authenticity. This removes Apple's verification
> layer and requires users to trust the software directly.
>
> Proper code signing and notarization is Apple's recommended method for
> distributing software. This approach should only be considered when code
> signing is not feasible.
>
> Important: Apple may disable this bypass method in future macOS versions
> without notice, potentially breaking software distribution that relies on it.

## Versioned Casks

{{< g_featpro >}}

GoReleaser can also create a versioned Cask.
For instance, you might want to make keep previous minor versions available to
your users, so they easily downgrade and/or keep using an older version.

To do that, use `alternative_names`:

```yaml {filename=".goreleaser.yaml"}
homebrew_casks:
  - name: foo
    alternative_names:
      - "foo@{{ .Major }}.{{ .Minor }}"
    # other fields
```

So, if you tag `v1.2.3`, GoReleaser will create and push `foo.rb` and
`foo@1.2.rb`.

Later on, you can tag `v1.3.0`, and then GoReleaser will create and push both
`foo.rb` (thus overriding the previous version) and `foo@1.3.rb`.
Your users can then `brew install foo@1.2` to keep using the previous version.

## GitHub Actions

To publish a cask from one repository to another using GitHub Actions, you
cannot use the default action token. You must use a separate token with content
write privileges for the tap repository. You can check the
[resource not accessible by integration](https://goreleaser.com/errors/resource-not-accessible-by-integration/)
for more information.

## Private GitHub Repositories

The best way to support private repositories is to define a custom
[`CurlDownloadStrategy`](https://rubydoc.brew.sh/CurlDownloadStrategy.html)
subclass in `custom_block` and reference it from `url.using`.

> [!WARNING]
> Homebrew 5.1.14 (May 2026) started
> [scrubbing sensitive environment variables](https://github.com/Homebrew/brew/pull/22384)
> — including `HOMEBREW_GITHUB_API_TOKEN` — for the duration of cask
> `instance_eval`.
>
> This means **you cannot read the token directly inside `url.template`
> or `url.headers`** anymore: those strings are interpolated at
> cask-load time, exactly when the token is hidden, and the cask fails
> with `Error: Failed to retrieve github api token` on every
> `brew update`/`install`/`upgrade`.
>
> The pattern below avoids that by deferring the token lookup to
> download time. `url.using` takes a **class reference**, so cask-load
> only captures the constant; Homebrew restores the environment before
> instantiating the strategy and calling `fetch`.

> [!WARNING]
> This example uses an internal Homebrew API (`GitHub.get_release`) to
> resolve the asset ID. Replace it with your own implementation as
> needed.

```yaml {filename=".goreleaser.yaml"}
homebrew_casks:
  - name: foo
    custom_block: |
      require "download_strategy"

      class GitHubPrivateRepositoryReleaseDownloadStrategy < CurlDownloadStrategy
        def initialize(url, name, version, **meta)
          @url = url
          parse_url_pattern
          set_github_token
          meta[:headers] ||= []
          meta[:headers] << "Accept: application/octet-stream"
          meta[:headers] << "Authorization: Bearer #{@github_token}"
          ghurl = "https://api.github.com/repos/#{@owner}/#{@repo}/releases/assets/#{asset_id}"
          super(ghurl, name, version, **meta)
        end

        def parse_url_pattern
          url_pattern = %r{https://github.com/([^/]+)/([^/]+)/releases/download/([^/]+)/(\S+)}
          raise CurlDownloadStrategyError, "Invalid url pattern" unless @url =~ url_pattern

          _, @owner, @repo, @tag, @filename = *@url.match(url_pattern)
        end

        def download_url
          "https://#{@github_token}@api.github.com/repos/#{@owner}/#{@repo}/releases/assets/#{asset_id}"
        end

        private

        def set_github_token
          @github_token = ENV["HOMEBREW_GITHUB_API_TOKEN"]
          raise CurlDownloadStrategyError, "HOMEBREW_GITHUB_API_TOKEN is required." unless @github_token

          # validate access
          GitHub.repository(@owner, @repo)
        end

        def asset_id
          @asset_id ||= GitHub.get_release(@owner, @repo, @tag)["assets"]
                              .find { |a| a["name"] == @filename }
                              .fetch("id") { raise CurlDownloadStrategyError, "Asset not found" }
        end
      end

    url:
      # Replace <owner>/<repo> with your private GitHub repository.
      template: "https://github.com/<owner>/<repo>/releases/download/{{ .Tag }}/{{ .ArtifactName }}"
      using: GitHubPrivateRepositoryReleaseDownloadStrategy
      verified: github.com/<owner>/<repo>/
```

Users still need `HOMEBREW_GITHUB_API_TOKEN` exported in their
environment — Homebrew restores it before the strategy runs.

{{% g_include file="includes/prs.md" %}}
