npx skills add ...
npx skills add connorads/dotfiles --skill homebrew-cask-authoring
Create, update, validate, and submit Homebrew Casks (macOS and Linux/AppImage). Use when the user mentions Homebrew cask/cask, Homebrew/homebrew-cask, adding a new cask, updating or bumping a cask, cask token naming, generate-cask-token, sha256, livecheck, zap/uninstall, AppImage/app_image, on_linux/on_macos, cross-platform cask, or when asked to run brew style, brew audit or brew lgtm for a cask.
npx skills add connorads/dotfiles --skill 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.
gh api repos/Homebrew/homebrew-cask/contents/AGENTS.md -H 'Accept: application/vnd.github.raw'.
Official docs: Cask Cookbook,
Acceptable Casks.url ..., verified:. It is deprecated and ignored; drop it from any cask you edit.depends_on :macos (or a versioned
depends_on macos: :<sym>) or depends_on :linux. Artifact type no longer implies the OS.on_macos (app, pkg, suite, ...) or on_linux (app_image); binary is
portable. Put depends_on macos: inside on_macos - at top level it drops Linux.rm, tap or system change before running it. Restore standard
Homebrew state after testing unless the user asks to keep the override.brew audit --new fails regardless of cask quality.spctl -a -vv <App>.app). Unsigned apps are ineligible; never suggest
--no-quarantine or disabling Gatekeeper as a workaround.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.<tag> is a GitHub pre-release. Add
"<token>": "all" to audit_exceptions/github_prerelease_allowlist.json
in 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.Start from brew generate-cask-token "<App Name>.app", then apply the judgement
rules it doesn't:
playonmac).executor for a desktop app,
with a later homebrew/core CLI taking executor-cli. The bare name goes to
whichever component lands in Homebrew first.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.cask token mentions desktop audit is strict-only (fires under --new).
Justify the choice in the PR body either way.@beta, @nightly, @latest, @<major>.Confirm the token with the user before writing the file.
Scaffold with brew create --cask <url> --set-name <token>, then trim to:
desc: factual, no marketing, no platform words ("for macOS"), under 80 chars.depends_on :macos for depends_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 no on_* blocks. Symbol table: the reference.Check the binary, not vendor marketing:
arm64 only: add depends_on arch: :arm64, or Intel users install an app they can't run.arch arm: ..., intel: ... plus sha256 arm: ..., intel: ....on_arm / on_intel blocks.uninstall is required for pkg and installer (pkgutil:, launchctl:, ...).uninstall quit: runs on uninstall, upgrade and reinstall; Homebrew reopens the
app after an upgrade. signal: is skipped on upgrade unless on_upgrade: :signal.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. Use
launchctl: "application.<bundle-id>.*" beside quit: (precedent:
shutter-encoder, cmux); removing the job ends the process.launchctl: checks each job again with sudo, 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.Contents/Helpers/, or pgrep -lf <AppName> while
running) needs every bundle ID in quit:. A wildcard works if the ID keeps at
least 3 dot-separated parts ("com.vendor.*").zap is optional for audit but expected by reviewers for new casks:
~/Library/HTTPStorages/<id>, session caches) only appear after use.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-zap covers ~/Library and ~/.<app> dotfolders, not XDG paths.
If state survives --zap + reinstall, grep upstream source for
os.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
~/Library for the token and bundle ID too, and grep upstream source for
path joins; some directories only appear once a feature is used.zap only, never uninstall.livecheck when the default check finds the version. Add a block only for
a demonstrated need (pre-releases, releases without assets, wrong source).brew find-appcast <path>.app.:github_latest / :github_releases are opt-in only; use them when Git tags or
an upstream feed won't work. They match tag_name, not asset names.strategy :extract_plist and version :latest are excluded from autobump
automatically; no no_autobump! needed.arch (intel: on_system_conditional(macos: "x64", linux: "x86_64")),
os, or an on_system_conditional local - all before version.sha256 keyed arm:, intel:, arm64_linux:, x86_64_linux:
(only the keys that exist). Never nest sha256 in on_macos/on_linux.on_macos / on_linux blocks go after sha256, before url.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.
Run from a local homebrew/cask checkout (setup: the reference):
brew audit is silent on success.brew lgtm diffs against the local main branch, not origin/main. In a
stale checkout it audits every cask upstream changed since (downloading their
artifacts). First git fetch origin && git rebase origin/main, then
git branch -f main origin/main; git diff --name-only main should list only your files.--online audits only the host OS/arch; --os=all --arch=all covers on_linux from a Mac.brew install asks for confirmation when it pulls
dependencies; pass -y.Validate zap with the app running:
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.
brew bump --open-pr <token> (or
brew bump-cask-pr <token> --version <new>). The manual flow is for new casks
and stanza changes.main.token 1.2.3 (new cask), token 1.2.3, or token: description.AGENTS.md). Squash only when a maintainer asks;
CONTRIBUTING.md and the docs describe squashing, so expect that request.Follow Responsible AI Usage and the template:
zap paths.quit: ID.Co-Authored-By, Assisted-by or similar AI trailers on commits.lipo -archs "/Volumes/<Vol>/<AppName>.app/Contents/MacOS/<AppName>"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>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: