Homebrew Cask Authoring
Would a homebrew-cask maintainer merge this without asking a question?
Homebrew's cops and audits enforce layout, ordering and most syntax. This skill
covers what they can't: eligibility, naming, cleanup paths, testing and PR
conduct. When a rule here and brew style/brew audit disagree, the tool wins -
it tracks Homebrew's current release, this file doesn't.
Operating rules
- Fetch homebrew-cask's own agent policy before a PR and follow it:
gh api repos/Homebrew/homebrew-cask/contents/AGENTS.md -H 'Accept: application/vnd.github.raw'. Official docs: Cask Cookbook, Acceptable Casks. - Keep casks minimal: only stanzas required for correct install, uninstall and cleanup.
- Never write
url ..., verified:. It is deprecated and ignored; drop it from any cask you edit. - A cask supporting one OS needs a top-level
depends_on :macos(or a versioneddepends_on macos: :<sym>) ordepends_on :linux. Artifact type no longer implies the OS. - A cross-platform cask has no top-level OS dependency. Gate OS-specific artifacts
inside
on_macos(app,pkg,suite, ...) oron_linux(app_image);binaryis portable. Putdepends_on macos:insideon_macos- at top level it drops Linux. - Call out any
rm, tap or system change before running it. Restore standard Homebrew state after testing unless the user asks to keep the override.
Pre-flight (new casks)
- Notability (policy): GitHub projects under 30 forks/watchers or 75 stars are likely rejected; 3x (90/90/225) when the PR author owns the upstream repo. Linux-only and AppImage casks get no exemption.
- Age: the repo and the homepage domain must be at least 30 days old, or
brew audit --newfails regardless of cask quality. - Gatekeeper: macOS artifacts must be signed and notarised
(
spctl -a -vv <App>.app). Unsigned apps are ineligible; never suggest--no-quarantineor disabling Gatekeeper as a workaround. - Rosetta: new
requires_rosetta/ x86_64-only macOS casks become ineligible once macOS 27 is the latest stable macOS. Check before drafting one. Linux-only casks are exempt. - Right repo: an open-source CLI with no compiled app belongs in homebrew/core. A homebrew/core rejection does not make it eligible as a cask.
- Prior art: search closed unmerged PRs and open PRs for the token. Don't resubmit a refusal for an unfixable reason.
- Linux build: check whether upstream ships an AppImage. Reviewers ask for it on new casks, so include it when it exists.
- Pre-releases: if upstream marks every GitHub release pre-release, the
online audit fails with
<tag> is a GitHub pre-release. Add"<token>": "all"toaudit_exceptions/github_prerelease_allowlist.jsonin the same commit (precedent:agent-tars,duplicati);"all"also errors if a non-pre-release appears later. Insert beside its neighbours - the file is not strictly sorted, so re-sorting it is a drive-by diff. Justify it in the PR body.
Workflow
1) Token
Start from brew generate-cask-token "<App Name>.app", then apply the judgement
rules it doesn't:
- Remove "Mac" unless the app is not a port and "Mac" is inseparable from the name (
playonmac). - Drop "Desktop" by default. A maintainer accepted
executorfor a desktop app, with a later homebrew/core CLI takingexecutor-cli. The bare name goes to whichever component lands in Homebrew first. - Keep "Desktop" only when it is the brand (
docker-desktop,ltx-desktop) or a sibling already exists in Homebrew under the bare name. A CLI that exists only upstream (npm, crates.io) is not a reason. - The
cask token mentions desktopaudit is strict-only (fires under--new). Justify the choice in the PR body either way. - Variants:
@beta,@nightly,@latest,@<major>.
Confirm the token with the user before writing the file.
2) Draft
Scaffold with brew create --cask <url> --set-name <token>, then trim to:
cask "token" do
version "1.2.3"
sha256 "..."
url "https://example.com/app-#{version}.dmg"
name "Official App Name"
desc "Short one-line description"
homepage "https://example.com/"
depends_on :macos
app "AppName.app"
end
desc: factual, no marketing, no platform words ("for macOS"), under 80 chars.- Swap
depends_on :macosfordepends_on macos: :<sym>when the app needs a newer floor.brew audit --cask --online --fix <token>derives it from the bundle, but only for casks with noon_*blocks. Symbol table: the reference.
3) Architecture
Check the binary, not vendor marketing:
lipo -archs "/Volumes/<Vol>/<AppName>.app/Contents/MacOS/<AppName>"
arm64only: adddepends_on arch: :arm64, or Intel users install an app they can't run.- Universal: no arch gate.
- Per-CPU downloads, same version:
arch arm: ..., intel: ...plussha256 arm: ..., intel: .... - Per-CPU versions:
on_arm/on_intelblocks.
4) uninstall and zap
uninstallis required forpkgandinstaller(pkgutil:,launchctl:, ...).uninstall quit:runs on uninstall, upgrade and reinstall; Homebrew reopens the app after an upgrade.signal:is skipped on upgrade unlesson_upgrade: :signal.- An app can ignore
quit:when a modal window (a first-run permissions panel) blocks its run loop. CI's zap-check launches the app on a fresh runner, so it hits this where a granted local install does not; CI then fails with "Some launch jobs were not unloaded".signal:does not help on macOS 26: Homebrew finds processes by launchd label, and the label now ends in a UUID (application.<id>.<n>.<n>.<UUID>) that its pattern rejects. Uselaunchctl: "application.<bundle-id>.*"besidequit:(precedent:shutter-encoder,cmux); removing the job ends the process. launchctl:checks each job again withsudo, so a local uninstall prompts for a password even for a user-level job. The non-sudo pass has already removed it; cancelling the prompt is safe. CI's sudo is passwordless.- An app with helper processes (
Contents/Helpers/, orpgrep -lf <AppName>while running) needs every bundle ID inquit:. A wildcard works if the ID keeps at least 3 dot-separated parts ("com.vendor.*"). zapis optional for audit but expected by reviewers for new casks:- Install, launch and use the app (log in, real work) - some paths
(
~/Library/HTTPStorages/<id>, session caches) only appear after use. - Run
brew generate-zap <token>(or--name "<App Name>"before the cask exists). If it errors asking for Full Disk Access, grant it to the terminal and rerun. Review the output; it includes noise. generate-zapcovers~/Libraryand~/.<app>dotfolders, not XDG paths. If state survives--zap+ reinstall, grep upstream source foros.homedir(),env-paths,xdg. It also matches only the app name, so it reports "No zap stanza required" when state is named after the CLI or token (~/Library/Caches/<cli>). Search~/Libraryfor the token and bundle ID too, and grep upstream source for path joins; some directories only appear once a feature is used.- After opening the PR, read CI's zap-check job summary for paths it thinks are missing.
- Install, launch and use the app (log in, real work) - some paths
(
- Keystone/GoogleUpdater-style shared components go in
zaponly, neveruninstall.
5) livecheck
- Omit
livecheckwhen the default check finds the version. Add a block only for a demonstrated need (pre-releases, releases without assets, wrong source). - For a Sparkle app, find its feed with
brew find-appcast <path>.app. :github_latest/:github_releasesare opt-in only; use them when Git tags or an upstream feed won't work. They matchtag_name, not asset names.strategy :extract_plistandversion :latestare excluded from autobump automatically; nono_autobump!needed.
6) Cross-platform (macOS + Linux AppImage)
- Per-OS strings live in top-level
arch(intel: on_system_conditional(macos: "x64", linux: "x86_64")),os, or anon_system_conditionallocal - all beforeversion. - One top-level
sha256keyedarm:,intel:,arm64_linux:,x86_64_linux:(only the keys that exist). Never nestsha256inon_macos/on_linux. on_macos/on_linuxblocks go aftersha256, beforeurl.app_image "<file>", target: "<App>.AppImage"- the target must not contain a version.
Templates (four-arch, single-arch Linux, universal macOS), the depends_on macos:
symbol table and app_image behaviour are in
references/homebrew-cask-contribution-workflow.md.
7) Validate
Run from a local homebrew/cask checkout (setup: the reference):
brew style --fix <token>
brew audit --cask --online --os=all --arch=all <token>
brew audit --cask --new <token> # new casks; implies --strict --online
brew lgtm --online # final gate; stage the cask first
HOMEBREW_NO_INSTALL_FROM_API=1 brew install --cask <token>
brew uninstall --cask <token>
brew auditis silent on success.brew lgtmdiffs against the localmainbranch, notorigin/main. In a stale checkout it audits every cask upstream changed since (downloading their artifacts). Firstgit fetch origin && git rebase origin/main, thengit branch -f main origin/main;git diff --name-only mainshould list only your files.- Plain
--onlineaudits only the host OS/arch;--os=all --arch=allcoverson_linuxfrom a Mac. - Install and uninstall by token, never by file path.
- On a TTY (tmux, agent PTY),
brew installasks for confirmation when it pulls dependencies; pass-y.
Validate zap with the app running:
HOMEBREW_NO_INSTALL_FROM_API=1 brew install --cask <token>
open "/Applications/<AppName>.app" # log in, use it
brew uninstall --zap --cask <token>
pgrep -lf <AppName> # empty, or add bundle IDs to uninstall quit:
Reinstall after plain uninstall should keep the login; after --zap it should
not. That confirms zap targets the real user state.
uninstall --zap uses the stanzas recorded at install time
(Caskroom/<token>/.metadata/), not your working copy. After editing zap:
uninstall, install, then uninstall --zap.
8) PR
- Version bump of an existing cask:
brew bump --open-pr <token>(orbrew bump-cask-pr <token> --version <new>). The manual flow is for new casks and stanza changes. - One cask per PR, minimal diff, no drive-by formatting. Base branch
main. - Commit subject (<=50 chars):
token 1.2.3 (new cask),token 1.2.3, ortoken: description. - One commit per cask when opening. After opening, push review and CI fixes as
new commits (homebrew-cask
AGENTS.md). Squash only when a maintainer asks; CONTRIBUTING.md and the docs describe squashing, so expect that request. - PR body: keep the template. Add one prose sentence above the checklist ("Adds a cask for App, a ..."), not a bare URL. Tick only what was done. No verbose logs or AI analysis.
9) AI disclosure
Follow Responsible AI Usage and the template:
- The human ticks the AI checkbox only after reviewing the output, including
zappaths. - Below it, briefly: the tool/model, how it was used, and what the human verified
by hand (install, login, use, zap derivation, running-app uninstall). Mention
anything non-obvious testing surfaced, such as a helper needing a second
quit:ID. - No
Co-Authored-By,Assisted-byor similar AI trailers on commits. - The human answers maintainer questions and review comments without AI. The agent does not draft or post them.
- Non-maintainers may have only one AI-assisted PR open at a time.