BrewMatch is a SwiftPM macOS CLI that scans installed .app bundles and reports which manually installed apps may have a Homebrew Cask replacement. It can also export a suggested Brewfile for review.
It is designed for local-first inventory and migration planning. It is dry-run by default; the only opt-in mutation path is the guarded Homebrew adopt command.
BrewMatch is early pre-1.0 software. Current version: 0.8.0.
The scanner, report output, ignore file, and Brewfile suggestion export are functional and covered by unit tests. Matching remains heuristic and should be reviewed before running brew bundle.
| Area | Current state |
|---|---|
| App scanning | Reads /Applications and ~/Applications, one level deep. |
| Metadata | Parses app Info.plist, MAS receipts, and local Homebrew Cask metadata. |
| Matching | Uses bundle identifiers, cask artifact names, normalized names, prefix matches, and fuzzy fallback. |
| Reports | Text and JSON scan reports with summary counts and warnings. |
| Brewfile export | Suggestion-only output for high-confidence matches by default. |
| App changes | Dry-run by default. Guarded adoption command exists for one explicit Homebrew adopt command only. |
- Dry-run by default.
- No direct app delete, move, or modify actions.
- The only mutation path is an explicit
brew install --cask --adopt <token>call after all adopt safety gates pass. - No credentials.
- No telemetry.
- No network calls except local
brewcommands. - Missing Homebrew is OK: BrewMatch still scans apps and reports matching as unavailable.
- Scans installed macOS
.appbundles. - Detects likely system apps and App Store apps.
- Detects Homebrew-managed casks when Homebrew is available.
- Finds possible Homebrew Cask replacements for manually installed apps.
- Produces plain text and JSON reports.
- Supports ignore files by bundle identifier, app name, or absolute path.
- Exports suggested Brewfile content for review.
- Keeps ambiguous suggestions commented, never active.
- Checks local readiness with
brewmatch doctor.
Build from source:
git clone https://github.com/me-cedric/BrewMatch.git
cd BrewMatch
swift build -c releaseInstall the release binary locally with the helper script:
./scripts/install-release.shPreview install without copying:
./scripts/install-release.sh --dry-runInstall into a custom prefix or bin directory:
./scripts/install-release.sh --prefix /usr/local
./scripts/install-release.sh --prefix "$HOME/.local"
./scripts/install-release.sh --bin-dir "$HOME/bin"The installer builds with SwiftPM, copies only the brewmatch binary, refuses to overwrite unless --force is passed, and never uses sudo automatically. If the destination is not writable, it prints a manual copy command for you to review.
Manual local install:
cp .build/release/brewmatch /usr/local/bin/brewmatchUninstall the local binary:
./scripts/uninstall-local.sh
./scripts/uninstall-local.sh --dry-runThe uninstall script removes only a file named exactly brewmatch at the configured path and never removes directories or uses sudo automatically.
Generate a local checksum:
./scripts/checksums.sh
cat dist/*.sha256Package local release artifacts:
./scripts/package-release.sh
(cd dist && shasum -a 256 -c brewmatch_Darwin_arm64.sha256)Safer local test before installing:
.build/release/brewmatch scanRun from SwiftPM:
swift run brewmatch scanUse built binary:
.build/release/brewmatch scanNo official Homebrew cask or tap exists yet. See docs/homebrew-tap-prep.md for draft tap preparation notes.
No official Homebrew tap is published yet. Use GitHub Release artifacts or the local install script until a tap is published.
Maintainer-only formula, tap scaffold, and manual publish plan tooling exists for preparation:
./scripts/package-release.sh
SHA256="$(./scripts/extract-release-sha.sh --file dist/brewmatch_Darwin_arm64.sha256)"
./scripts/generate-homebrew-formula.sh --version 0.8.0 --sha256 "$SHA256" --output dist/homebrew/brewmatch.rb --force
./scripts/validate-homebrew-formula.sh --formula dist/homebrew/brewmatch.rb --version 0.8.0 --sha256 "$SHA256"
./scripts/scaffold-homebrew-tap.sh --version 0.8.0 --sha256 "$SHA256" --output-dir dist/homebrew-tap --force
./scripts/validate-homebrew-tap.sh --tap-dir dist/homebrew-tap --version 0.8.0 --sha256 "$SHA256"
./scripts/plan-homebrew-tap-publish.sh --version 0.8.0 --tap-repo me-cedric/homebrew-brewmatch --sha256 "$SHA256" --output dist/homebrew-tap-publish-plan.txt --force
./scripts/validate-homebrew-tap-publish-plan.sh --plan dist/homebrew-tap-publish-plan.txt --version 0.8.0 --tap-repo me-cedric/homebrew-brewmatch --sha256 "$SHA256"v0.9 adds publish plan generation only. These scripts do not run brew, publish a tap, initialize git, or push to a tap repository. See docs/homebrew-formula.md, docs/homebrew-tap-scaffold.md, and docs/homebrew-tap-publish-plan.md.
| Command | Purpose |
|---|---|
brewmatch doctor |
Check local readiness without modifying apps. |
brewmatch doctor --json |
Print machine-readable doctor checks. |
brewmatch scan |
Scan apps and print a text report. |
brewmatch scan --json |
Scan apps and print JSON. |
brewmatch scan --include-system |
Include /System/Applications in scan roots; system apps remain skipped. |
brewmatch scan --output report.json |
Export JSON report based on .json extension. |
brewmatch report --output report.txt --force |
Export text report and overwrite existing file. |
brewmatch report --explain |
Include confidence reasoning and candidate match reasons. |
brewmatch brewfile |
Print suggested Brewfile content. |
brewmatch brewfile --with-comments --output Brewfile |
Export commented Brewfile suggestions. |
brewmatch suggestions |
Alias for brewmatch brewfile --with-comments. |
brewmatch plan |
Print a dry-run migration plan. No actions are executed. |
brewmatch plan --strict |
Include only low-risk proposed entries; move other candidates to review. |
brewmatch plan --explain |
Include detailed reasoning, source classification, and risk notes. |
brewmatch plan --with-commands |
Include proposed commands as dry-run text only. |
brewmatch adopt |
Dry-run adopt planning. No actions are executed by default. |
brewmatch adopt --cask firefox |
Show the selected dry-run adoption candidate. |
brewmatch adopt --cask firefox --dry-run |
Explicit dry-run; same as default. |
brewmatch adopt --cask firefox --execute --confirm "adopt firefox" --i-understand-this-may-change-my-system |
Execute only if every safety gate and preflight check passes. |
suggestions is an alias for brewfile --with-comments.
Report export:
brewmatch scan --output report.json
brewmatch report --output report.txt --forceJSON output includes raw app fields, match reasons, warnings, and summary counts:
brewmatch scan --jsonbrewmatch doctor checks local readiness without modifying applications.
brewmatch doctor
brewmatch doctor --json
brewmatch doctor --json --output doctor.json --forceDoctor checks macOS version, architecture, Homebrew path/version, cask metadata support, app directory readability, ignore file validity, temporary output writing, and whether an app scan can run. It is read-only for applications and does not install, adopt, delete, move, or modify apps. It may create and remove one temporary file in the system temp directory for the write check.
Doctor exits 0 when checks pass with warnings or better, and non-zero when a check fails. See docs/doctor-json-schema.md for JSON fields.
brewmatch report --explain adds confidence details:
brewmatch report --explainExplain output names the match source: bundle identifier, cask artifact app name, cask token, normalized name, or fuzzy fallback.
brewmatch scan --include-system adds /System/Applications to scan roots. System apps are still classified as skipped system apps and are never adopt candidates.
By default, Brewfile export includes only high-confidence, non-ambiguous cask matches:
# BrewMatch suggested Brewfile
# Generated from local macOS application scan
# Review before running brew bundle
# Active casks are high-confidence non-ambiguous suggestions by default
cask "firefox"Options:
--include-medium--include-low--include-ambiguous--with-comments--no-header--output <path>--force--ignore-file <path>
Ambiguous matches are commented when included:
# Ambiguous: Cursor.app
# candidate: cursor confidence: medium reason: normalized app name match
# candidate: cursor-cli confidence: medium reason: token prefix match
# cask "cursor"brewmatch plan prepares a dry-run migration plan for apps that may be movable under Homebrew management later.
It does not run brew install --cask --adopt, install casks, delete apps, move apps, or modify apps.
brewmatch plan
brewmatch plan --json
brewmatch plan --strict
brewmatch plan --strict --with-commands
brewmatch plan --explain
brewmatch plan --with-commands
brewmatch plan --output plan.json --json --force
brewmatch plan --json --output plan.json --forceEvery plan includes:
No actions will be executed.
Risk levels:
low: high confidence from an exact bundle identifier match.medium: high confidence from app name, artifact name, or token matching.review-required: medium or low confidence, ambiguous candidates, App Store apps, and system apps.
Plan statuses:
proposed: low/medium risk high-confidence candidates, unless--strictexcludes medium risk.reviewRequired: candidates that should be manually checked.skipped: system, App Store, ignored, already managed, no-match, or threshold-excluded apps.
--strict keeps only low-risk entries as proposed and marks medium-risk candidates as review-required with excluded by strict mode.
Exact adopt commands are shown only when --with-commands is passed. Proposed entries render active command text. Review-required entries render commented command text:
# review required: brew install --cask --adopt cursorThese commands are never executed by BrewMatch. See docs/plan-json-schema.md for machine-readable plan fields.
When --with-commands is passed, plan output also includes Copyable commands with only low-risk proposed commands. Review-required commands stay commented and out of that section.
brewmatch adopt is a guarded foundation for future Homebrew Cask adoption. Default behavior is dry-run.
brewmatch adopt
brewmatch adopt --cask firefox
brewmatch adopt --cask firefox --dry-run
brewmatch adopt --app Firefox.app
brewmatch adopt --json --output adopt.json --force
brewmatch adopt --cask firefox --audit-log adopt-audit.json
brewmatch adopt --cask firefox --require-clean-plan --explain
brewmatch adopt --cask firefox --with-commands
brewmatch adopt --cask firefox --execute --confirm "adopt firefox" --i-understand-this-may-change-my-system
brewmatch adopt --cask firefox --execute --confirm "adopt firefox" --i-understand-this-may-change-my-system --require-clean-plan --audit-log ./adopt-audit.jsonDry-run output may show the exact command that would be used:
brew install --cask --adopt firefoxBrewMatch never deletes or moves apps directly. Real execution only shells out to:
brew install --cask --adopt <token>BrewMatch does not run that command unless all safety gates pass:
--executeis present.--dry-runis not present.- Exactly one cask token or app selector is provided.
- Selected entry is
proposed. - Selected entry is
lowrisk. - Selected match confidence is
high. - Selected app is not ignored, not App Store, not system, and not ambiguous.
- Confirmation phrase exactly matches
--confirm "adopt <token>". - User also passes
--i-understand-this-may-change-my-system. - If stdin is interactive, user types exact final prompt response
ADOPT. - Command arguments are exactly
["install", "--cask", "--adopt", "<token>"].
Before execution, BrewMatch also runs preflight checks:
- Homebrew is available.
- The selected cask token resolves through Homebrew metadata or cask search.
- The app still exists at the scanned path.
- The app is not already Homebrew-managed.
- The bundle identifier still matches the scanned app, when available.
--require-clean-plan adds stricter execution gates. It blocks execution when the current scan has review-required entries, warnings, or the selected app has alternative candidates. In dry-run mode, --explain shows this gate without executing.
--audit-log <path> writes a JSON audit object for dry-run, blocked, and executed runs. It refuses to overwrite existing files unless --force is passed.
--with-commands adds Copyable commands for low-risk proposed dry-run commands only. Review-required commands are excluded.
Blocked or dry-run output always includes:
No actions were executed.
Default path:
~/.config/brewmatch/ignore.jsonSupported formats:
[
"com.example.CustomApp",
"Custom App.app",
"/Applications/Custom App.app"
]Or grouped:
{
"bundleIdentifiers": ["com.example.CustomApp"],
"names": ["Custom App.app"],
"paths": ["/Applications/Custom App.app"]
}Ignored apps stay visible in reports under Ignored.
BrewMatch/
├── Sources/BrewMatch/
│ ├── AppScanner.swift # read-only .app discovery and Info.plist parsing
│ ├── BrewClient.swift # local brew command wrapper and cask metadata parsing
│ ├── Matcher.swift # cask confidence scoring
│ ├── Reporter.swift # scan report assembly and text/JSON rendering
│ ├── BrewfileRenderer.swift # suggestion-only Brewfile output
│ ├── MigrationPlan.swift # dry-run migration planning and plan JSON
│ └── main.swift # CLI entry point
└── Tests/BrewMatchTests/
Core boundaries:
AppScanneronly reads filesystem metadata.BrewClientonly calls localbrewcommands and returns parsed data.Matcheris pure matching logic.ReporterandBrewfileRendererturn scan results into output.- Tests use mocked Homebrew clients and temporary directories.
swift package describe
swift test
swift buildUnit tests do not require Homebrew to be installed.
Shortcut targets:
./scripts/validate.sh
./scripts/smoke-release.sh
make validate
make smoke
make build
make test
make cleanThe Release Build GitHub Actions workflow builds release artifacts on tag pushes matching v*.*.* and on manual runs. It uploads:
brewmatchbrewmatch_Darwin_arm64.tar.gzbrewmatch_Darwin_arm64.sha256
Tag pushes may create or update a GitHub Release object with the tarball and checksum. Manual workflow runs do not create a GitHub Release unless create_release is explicitly set to true. The workflow does not publish a Homebrew tap and does not require custom secrets.
Local distribution helpers:
./scripts/package-release.sh
./scripts/verify-release-version.sh --version 0.8.0 --binary dist/brewmatch
./scripts/install-release.sh --dry-run
./scripts/uninstall-local.sh --dry-runChecksum verification:
cd dist
shasum -a 256 -c brewmatch_Darwin_arm64.sha256Draft release notes live in docs/release-notes. Artifact details live in docs/release-artifacts.md. Generated dist/ artifacts are ignored by git.
- Matching is heuristic.
- Homebrew metadata depends on local
brew info --json=v2 --cask. - Scanner only searches
/Applicationsand~/Applications, one level deep. - Brewfile output is suggestion-only. Review before running
brew bundle.
- More cask metadata fields.
- Better duplicate app grouping.
- Optional config file.
- GUI later, after CLI behavior stabilizes.
Contributions are welcome. Read CONTRIBUTING.md before opening a PR.
Keep changes aligned with the read-only safety model:
- no app modification,
- no telemetry,
- no secrets,
- no Homebrew dependency in tests.
BrewMatch reports may include app names, bundle identifiers, versions, and local paths. Treat JSON reports as potentially sensitive and sanitize them before sharing.
See SECURITY.md for reporting guidance.
macOS, Swift, SwiftPM, command line, CLI, Homebrew, Homebrew Cask, Brewfile, brew bundle, application inventory, app migration, local-first, privacy, read-only.
MIT. See LICENSE.