This document helps coding agents produce high-quality PRs for homebrew-core formula contributions.
- Check for existing PRs for the same formula: open PRs
- Run
brew tap homebrew/coreif not already tapped
- Treat a tap vs
homebrew/coreoverlap as real only when both formulae point to the same upstream project. - For quick screening, compare exact formula name plus
urlanddesc; a name match alone is not enough. - Re-verify any collision whose
urlordescdiffers before proposing removal, rename, or dedupe work. - Known exceptions in this tap:
hellois an intentional overlap and should be kept because it is used to test the tap formula infrastructure.kafkais an intentional overlap because this tap needs Kafka 3.9.zookeeperis an intentional overlap because this tap needs a JDK 21 build; thehomebrew/coreopenjdk-based formula does not work for this use case.cartonis a name-only collision, not a real overlap: this tap packagesswiftwasm/carton, whilehomebrew/corecartonis the Perl CPAN dependency manager.
Preferred method for version bumps:
brew bump-formula-pr --strict <formula> --url=<url> --sha256=<sha256>
# or
brew bump-formula-pr --strict <formula> --tag=<tag> --revision=<revision>
# or
brew bump-formula-pr --strict <formula> --version=<version>This handles URL/checksum updates, commit message, and opens the PR automatically.
If manual editing is needed:
brew edit <formula>
# Update url and sha256 (or tag and revision)
# Leave `bottle do` block unchangedCommit message: foo 1.2.3
For bug fixes or improvements to existing formulae:
brew edit <formula>
# Make changes
# Leave `bottle do` block unchangedCommit message: foo: fix <description> or foo: <description>
Prefer Pathname idioms where possible.
Examples:
session_dir.mkpath
bin.install_symlink libexec.glob("bin/*")- Declare
depends_on "pkgconf" => :buildat the top level by default.pkgconfis a build tool, so keep it OS-agnostic unless the formula has a verified platform-specific build path that never invokespkg-configelsewhere. - Keep platform guards for the libraries that are actually platform-specific, such as Linux-only
gliborlibsecretdependencies.
When a Python formula can reuse a packaged dependency from Homebrew instead of vendoring it as a resource, prefer the shared formula dependency.
- For Pydantic v2 consumers, prefer:
depends_on "pydantic" => :no_linkage
- Do NOT add
depends_on "pydantic-core": there is no standalonepydantic-coreformula in Homebrew;pydantic-coreis provided by thepydanticformula. - Remove vendored
pydantic,pydantic-core, and their helper resources when they are satisfied by the sharedpydanticformula. - If the formula uses
pypi_packages, exclude shared Python formula deps there as well so autobump can manage the remaining vendored resources cleanly. For example:depends_on "certifi" => :no_linkage depends_on "pydantic" => :no_linkage pypi_packages exclude_packages: %w[certifi pydantic]
- Apply the same exclusion pattern to any other shared Python deps moved out of resources, such as
cryptographyorrpds-py. - Prefer source tarballs for Python formula resources. Do not switch resources to wheels just to bypass isolated-build failures; fix the source build inputs or use shared Homebrew dependencies instead. Wheels are acceptable only when upstream has no usable sdist or there is a separately verified packaging reason.
- Do NOT use
uses_from_macos "zlib". - Prefer:
on_linux do depends_on "zlib-ng-compat" end
- Keep this Linux-only unless the formula needs a separate macOS change for other reasons.
Add or increment revision when:
- Fix requires existing bottles to be rebuilt
- Dependencies changed in a way that affects the built package
- The installed binary/library behavior changes
Do NOT add revision for cosmetic changes (comments, style, livecheck fixes).
brew create <url>
# Edit the generated formulaCommit message: foo 1.2.3 (new formula)
- Build source policy: MUST build from source in the formula (e.g.,
go build,cargo install,cmake, etc.).- Do NOT package upstream prebuilt binaries/releases for formula installation.
- If upstream only ships binaries and no buildable source path, raise it for manual review instead of adding the formula.
- Go formulae that need to override Homebrew's default cgo behavior should prefer:
ENV["CGO_ENABLED"] = "1" if OS.linux? && Hardware::CPU.arm?
- Do NOT set
ENV["CGO_ENABLED"] = "1"unconditionally in Go formulae unless the formula has a separately verified requirement outside Linux ARM. - Rust binary formulae MUST use
cargo installwithstd_cargo_args(for examplesystem "cargo", "install", *std_cargo_args). - When the crate root is the current directory, use bare
*std_cargo_argsand do NOT passpath: ".". - Reserve
std_cargo_args(path: "...")for crates that live in a subdirectory. - Do NOT hand-roll standard Rust binary installs with
cargo build+bin.installwhenstd_cargo_argsapplies. - Do NOT manually append
--lockedor--pathwhenstd_cargo_argsis used.
- Test block: MUST verify actual functionality, not just
--versionor--help- Include a version assertion as an additional check whenever a reliable version command/output exists
- Prefer the simple standard form:
assert_match version.to_s, shell_output("#{bin}/foo --version") - Avoid regex-only version assertions when
version.to_smatching is available - For libraries: compile and link sample code
- Use
testpathfor temporary files - Do NOT override
HOMEintest dowhen Homebrew already provides the isolated test home. - In particular, do NOT set
ENV["HOME"] = testpathorENV["HOME"] = testpath.to_sintest do. - When a formula genuinely needs a custom
HOMEoutsidetest do(for example during install-time completion generation), scope it to the smallest possible block withwith_envor a command-scoped override instead of mutating globalENV. - Prefer no
HOMEoverride whenever possible.
- Completions policy: Add shell completion support when upstream CLI supports it.
- Use Homebrew DSL:
generate_completions_from_executable. - Rust CLIs: only use
shell_parameter_format: :clapwhen the binary supports Homebrew'sCOMPLETE=<shell>invocation; otherwise keep the explicit"completion"or"completions"subcommand form. - Go CLIs: prefer
shell_parameter_format: :cobra. - Python CLIs: prefer
shell_parameter_format: :clickor:typer(based on upstream framework).
- Use Homebrew DSL:
- For shell plugins that are not standalone executables (for example Oh My Zsh or Bash plugin repos), package the plugin assets with
pkgshare.installinstead of pretending the repo is a normal binary formula. - Prefer adding a small installer wrapper in
bin/when upstream's install story is "copy these plugin files into a plugin directory". - For Oh My Zsh-style plugins, use a deterministic test with a fake
ZSH_CUSTOMdirectory undertestpath, run the installer wrapper, and assert that the plugin files were copied into the expected plugin directory. - If the plugin needs multiple files (for example
*.plugin.zsh, helper*.zsh, or alib/directory), install and copy the full runtime set; do not package only the entrypoint file. - Add caveats that point users to the installer wrapper or the
pkgsharesource path instead of telling them to clone the repo manually.
-
Prefer installing shared libraries (
.dylib/.so) when upstream supports both shared and static builds. -
Avoid static-only installs unless upstream cannot build shared libraries, or there is a clear technical reason documented in the formula.
-
If upstream lacks
install()rules, manual installation is acceptable, but still prefer installing the shared artifact when available. -
Service block: If the software can run as a daemon, include a
service doblock.- Place
service doafterdef installand beforetest do.
- Place
service do run [opt_bin/"foo", "start"] keep_alive true end
- **Livecheck**: Prefer default behavior. Only add a `livecheck` block if automatic detection fails.
- **Head support**: Include when the project has a development branch:
```ruby
head "https://github.com/org/repo.git", branch: "main"
Git repositories MUST specify branch:.
- If local tap intake is blocked only because the current environment falsely rejects an otherwise-valid GitHub
headURL, prefer omittingheadin this tap rather than stalling the formula on local transport validation noise.
All checks MUST pass locally before opening a PR:
# Build from source (required)
HOMEBREW_NO_AUTO_UPDATE=1 HOMEBREW_NO_INSTALL_FROM_API=1 brew install --build-from-source <formula>
# Run tests
brew test <formula>
# Linkage check
brew linkage --test <formula>
# Audit (existing formula)
brew audit --strict <formula>
# Audit (new formula only)
brew audit --new <formula>
# Style check
brew style <formula>- Any formula PR that is not labeled
CI-syntax-onlyMUST go through thepr-pullprocess.- This includes new formulae, version bumps, revision rebuilds, and formula fixes that should produce or refresh bottles.
- After checks pass, wait for the test workflow to add
pr-pull, then let thebrew pr-pullworkflow merge the PR. - Do NOT manually merge these PRs with
gh pr merge, because that bypasses BrewTestBot bottle commits and can leavemainwithout abottle doblock.
- Never force-push
maintomain.git push --force-with-leaseis only for PR head branches that you explicitly verified are notmain.- When updating
main, use a normalgit push origin main. - If local
mainandorigin/maindiverge, rungit pull --rebase origin main, resolve conflicts locally, and then push normally.
- Manual merges are acceptable only for PRs explicitly labeled
CI-syntax-only, meaning CI should run syntax checks only and no bottle-producing build should occur. - If a new formula lands on
mainwithout abottle doblock, open a one-formula follow-up PR that only adds or incrementsrevisionto force a fresh bottle build, and again leave that PR for the bot-managedpr-pullmerge path.
For formula patch PR triage, follow this exact sequence:
- Run brew ops and ensure all pass:
HOMEBREW_NO_AUTO_UPDATE=1 HOMEBREW_NO_INSTALL_FROM_API=1 brew install --build-from-source <formula> brew test <formula> brew linkage --test <formula> brew audit --strict <formula> # or --new for new formulae brew style <formula>
- If any step fails, patch the PR branch with the smallest formula fix and rerun the full brew-ops chain until all steps pass.
- When testing on Linux/macOS
*.upterm.devremote runners, do not runexit,logout, or close the session after brew ops; keep the runner alive for follow-up commands. - When using rerun-backed debug workflows, always attach to the current attempt's job/session rather than an older rerun attempt.
- On a fresh remote runner connection, start with harmless probes such as
pwdanduname -abefore heavier commands. - Do not lead with
set -euo pipefailbefore confirming the runner's working directory and the paths you plan to use actually exist on that runner. - Do not reference workstation-local paths such as
/private/tmp/...or/Users/...on the runner; use heredocs,scp, or create the needed files directly on the runner first.
- Commit on the PR head branch with a short, concise formula patch:
branch="$(gh pr view --json headRefName -q .headRefName)" git switch "$branch" git add Formula/<path>/<formula>.rb git commit -m "<formula>: <short fix>"
- Squash commits while preserving the BrewTestBot-compatible commit subject header:
base="$(gh pr view --json baseRefName -q .baseRefName)" header="$(git log --reverse --format=%s "origin/${base}..HEAD" | head -n1)" # Squash as needed, but keep the final first line equal to "$header" git log -1 --pretty=%s
- Force-update the PR head branch safely:
test "$branch" != main git push --force-with-lease origin "$branch"
- If a global Git config rewrites
https://github.com/pushes togit@github.com:and SSH auth is unavailable, useenv GIT_CONFIG_GLOBAL=/dev/null git push -u https://github.com/<owner>/<repo> "$branch"instead of rewritingorigin.
- If a global Git config rewrites
- Mark the PR with
CI-no-fail-fast:pr="$(gh pr view --json number -q .number)" gh pr edit "$pr" --add-label CI-no-fail-fast
- For any formula PR not labeled
CI-syntax-only, stop after the branch is green and labeled correctly, then leave merge to the bot-managedpr-pullworkflow.- Do NOT use
gh pr mergemanually for formula PRs that should produce bottles. - If the goal is to regenerate missing bottles for a merged formula, open a one-formula
revisionfollow-up PR and again leave merge topr-pull.
- Do NOT use
- If triaging many open PRs, dedupe only version-bump PRs for the same formula by keeping only the latest one.
- Apply this only to PR titles in version-bump format (
<formula> <version>), and skip non-version PRs such asfoo: fix .... - Prefer
brew close-superseded-prs --applyfor this cleanup when it fits; it dry-runs by default and handles both PRs already covered bymainand older open bump PRs superseded by a more recently opened bump.
repo="<owner>/<repo>" gh pr list --repo "$repo" --state open --limit 1000 --json number,title,createdAt > /tmp/open_prs.json jq -r ' # Version-bump titles only: "<formula> <version>" map(select(.title | test("^[^: ]+ [0-9]"))) | sort_by(.createdAt) | group_by(.title | capture("^(?<formula>[^ ]+) ").formula)[] | select(length > 1) | (.[-1].number | tostring) as $keeper | .[0:-1][] | "\(.number) \($keeper)" ' /tmp/open_prs.json > /tmp/superseded_pr_pairs.txt
- Apply this only to PR titles in version-bump format (
- For each older PR, comment + label + close:
repo="<owner>/<repo>" while read -r old_pr keeper_pr; do [ -n "$old_pr" ] || continue printf 'Superseded by #%s\n' "$keeper_pr" > "/tmp/pr-${old_pr}-superseded.md" gh pr comment "$old_pr" --repo "$repo" --body-file "/tmp/pr-${old_pr}-superseded.md" gh pr edit "$old_pr" --repo "$repo" --add-label superseded gh pr close "$old_pr" --repo "$repo" done < /tmp/superseded_pr_pairs.txt
You MUST verify all items before submitting:
- Followed CONTRIBUTING.md
- Commits follow commit style guide
- No existing open PRs for same change
- Built locally with
HOMEBREW_NO_INSTALL_FROM_API=1 brew install --build-from-source - Tests pass with
brew test - Linkage passes with
brew linkage --test - Audit passes with
brew audit --strict(or--newfor new formulae) - Style passes with
brew style
- Version update:
foo 1.2.3 - New formula:
<formula_name> <version> (new formula)- Example:
ls-hpack 2.3.4 (new formula)
- Example:
- Fix/change:
foo: fix <description>orfoo: <description> - First line MUST be 50 characters or less
- Reference issues with
Closes #12345in commit body if applicable
- One formula or cask change per PR — always create a dedicated branch and open a separate PR for each formula/cask, even when working on multiple in the same session
- Never commit formula or cask changes directly to
main; always use a PR branch - Keep diffs minimal and focused
- Provide only essential context in PR description
- For any formula PR not labeled
CI-syntax-only, use thepr-pullmerge path so BrewTestBot adds the bottle commit tomain
- Edit
bottle doblocks (managed by BrewTestBot) - Batch unrelated formula or cask changes into a single PR
- Include large logs or verbose output in PR body
- Add non-Homebrew usage caveats in PR body
- Include unrelated refactors or cleanups
- Manually merge formula PRs that are not labeled
CI-syntax-onlywithgh pr merge
Keep it minimal:
Built and tested locally on [macOS version/Linux].
[One sentence describing the change if not obvious from title.]
When using gh to create/edit PRs or issues:
- Prefer
--body-filewith a heredoc-generated markdown file to preserve newlines. - Avoid passing escaped
\nin quoted--bodystrings. - If inline body text is required, use single quotes around the full body to avoid shell interpolation.
For recurring maintenance work in this tap, prefer the repo-local helpers under cmd/ when they fit:
brew migrate-python <formula>- For tap-local Python migration PRs.
- Works against
chenrui333/tap, refreshes resources withbrew update-python-resources2, pushes the branch, and opens the PR.
brew check <formula>- Shortcut for
brew audit --strict --git --online --fix. - Bare formula names are resolved to
chenrui333/tap/<formula>.
- Shortcut for
brew patch <url>- Fetches a patch URL, computes the SHA-256, and prints a
patch doblock for formula edits.
- Fetches a patch URL, computes the SHA-256, and prints a
brew close-superseded-prs- Dry-runs stale formula bump cleanup by default.
- With
--apply, comments, labelssuperseded, and closes formula bump PRs that are covered bymainor superseded by a more recently opened bump PR for the same formula.
If a helper does not match the job cleanly, fall back to the explicit brew/gh commands in this document instead of forcing the helper into a workflow it was not built for.
- Formula version bumps are owned by autobump-formula.yml. Keep Renovate from opening
Formula/**update PRs; if Renovate proposes a formula update, close it as a duplicate of the BrewTestBot/autobump PR or update .github/renovate.json5. - Do not add
Casks/**to Renovate ignore rules just to mirrorFormula/**. Renovate's current Homebrew manager does not scan casks, and leavingCasks/**visible preserves future native cask support. Cask bumps remain owned by autobump-cask.yml unless the repo intentionally changes policy.
- Reproduce failures locally before debugging
- Read error messages and annotations in "Files changed" tab
- Check complete build log in "Checks" tab if needed
- For Linux failures, use the Homebrew Docker container
- If stuck, comment describing what you've tried
- For tap PR check refreshes, use the repo-local skill at
skills/restart-github-actions-runs/SKILL.md. - Prefer the helper:
skills/restart-github-actions-runs/scripts/restart_pr_actions.sh --repo chenrui333/homebrew-tap <pr> [<pr> ...]
- The helper prefers a safe empty-amend +
git push --force-with-leaseon verified same-repo PR head branches, and falls back togh run rerunwhen the head branch is missing or otherwise not safe to push. - If GitHub HTTPS pushes are being rewritten to SSH by global git config and SSH auth is unavailable, run the helper or any manual PR-branch push under
env GIT_CONFIG_GLOBAL=/dev/nullinstead of changingorigin. - Never edit workflow files just to restart checks.
- Never force-push
main.
- For GitHub package visibility changes, use the repo-local skill at
skills/github-package-visibility/SKILL.md. - Use Playwright MCP with an existing signed-in browser session when available.
- Verify the owner/account context first, change one package before batching, and keep committed examples generic.
- Confirm completion from the filtered
privatepackages view after the run.
If AI assisted with the PR, check the AI checkbox in the PR template and briefly describe:
- How AI was used
- What manual verification was performed