A security-focused Homebrew wrapper that holds back package upgrades so supply chain attacks, malicious releases, and botched versions have a chance to get caught before they reach your machine.
Don't get cut by the bleeding edge.
brew upgrade installs whatever is current right now. That's fine until a
formula gets compromised, a publisher pushes a broken release, or a typosquat
slips into a tap. By the time the incident is public, the bad version is
already on your system.
cold-brew delays every upgrade by a configurable hold window (default 7 days). During that window the package is "held" — cold-brew will not upgrade it, but it continues to watch advisory feeds. If a package you're holding turns out to have a serious vulnerability, cold-brew promotes it out of the hold window automatically so security fixes aren't stuck behind the safety delay.
You still get upgrades. You just get them a week late, by default, instead of the minute they ship.
- Hold window. A package becomes upgradable once its upstream source file
is at least
hold-daysold (default7). Publish dates come from GitHub's commit metadata on the formula, not local install time, so freshly installed packages are evaluated against the true upstream age. - Advisory feeds. On every run, cold-brew queries
OSV and GitHub Security Advisories (GHSA) for the
installed versions and caches results in SQLite. OSV
MAL-*entries that match a Homebrew formula name are surfaced separately as typosquat alerts (informational — they never auto-bypass). - Auto-bypass. If a held package has an actionable advisory (a known fix
exists in the latest version) whose max CVSS meets or exceeds the
auto-bypass-cvssthreshold (default7.0), cold-brew promotes it fromHeldtoReadyso you pick up the fix immediately. - Stepping. When a package is held but version history is available, cold-brew can suggest an intermediate version — the newest release that is (a) newer than what you have, (b) older than latest, and (c) past the hold window — so you move forward safely without jumping to the bleeding edge.
- Per-package policies.
always-hold,always-allow, and explicit version pins override the global hold window for specific packages.
Status is recomputed from live Homebrew data on every run; the cache is only ever a supplement, never the source of truth.
Every outdated package is assigned exactly one of the statuses below. The
same labels appear in cold-brew status, cold-brew audit, and the TUI.
| Status | Meaning |
|---|---|
ready |
Hold window satisfied — eligible for upgrade. |
held |
Inside the hold window — upgrade deferred. |
always allowed |
Per-package policy: upgrade immediately, regardless of the hold window. |
always held |
Per-package policy: never auto-upgrade. |
pinned by brew |
brew pin is set — Homebrew itself will refuse to upgrade. |
pinned to X.Y.Z |
cold-brew version pin — sticks to a specific version. |
ahead of upstream |
Installed version is newer than upstream's current latest (rollback). |
bypassed |
Promoted from held to ready because a fixed advisory met the CVSS threshold. |
cold-brew was designed against the shape of real incidents, not hypothetical ones. Below is a non-exhaustive list of public supply-chain events from the last few years and the specific mechanism above that would have applied. The goal is to make the value proposition concrete — not to claim cold-brew is a substitute for code review or vendor trust.
-
OSV
MAL-*typosquat feed (ongoing). OSV publishes a continuous stream of malicious-package advisories — typosquats, dependency confusion, hijacked maintainer accounts. cold-brew protection:MAL-*entries matching a Homebrew formula name are surfaced as typosquat alerts incold-brew audit. Honest caveat: informational only — they never auto-bypass and they never block abrew install(those route around cold-brew with a warning). The value is awareness before you type the install command. -
TeamPCP / CanisterWorm cluster (early 2026), including the
axiosRAT and the@bitwarden/cli@2026.4.0typosquat. A clustered actor (overlapping per-source attribution to a North Korea–nexus group) hijacked maintainer tokens and dropped infostealers across Trivy, KICS, LiteLLM, Telnyx, Checkmarx,axios, and an impostor publish of@bitwarden/cli. The maliciousaxiosversions were live for roughly three hours before npm pulled them. cold-brew protection: thebitwarden-cliHomebrew formula is the cleanest direct hit — any formula commit bumping it to the trojaned version stays inHelduntilhold-dayselapse, by which point upstream had already yanked. Theaxiosexposure is indirect, via Homebrew-installable Node CLIs that bundle it. -
Shai-Hulud npm worm (September 2025) and "Shai-Hulud 2.0" (November 2025). Self-propagating npm worm that stole maintainer tokens and republished trojaned versions of every package its victim controlled — ≈500 packages in the first wave (
@ctrl/tinycolor, CrowdStrike OSS, others) and ≈795 in the second (Zapier, PostHog, Postman, AsyncAPI). The v2 variant moved to apreinstallhook and tried to wipe the home directory if it could not authenticate. cold-brew protection: most malicious versions were yanked within hours and effectively all within ≈5 days, well inside a 7-day hold for any Homebrew formula or cask whose upstream tarball re-bundled them. Honest caveat: cold-brew is a Homebrew wrapper — for direct npm/PyPI/RubyGems exposure you need ecosystem-specific tooling on top. -
xz-utils backdoor — CVE-2024-3094 (March 2024). A long-running maintainer takeover planted a backdoor in
xz5.6.0 and 5.6.1;xzships inhomebrew-core. cold-brew protection: once OSV/GHSA flagged the bad versions, anyone still on 5.4.x would be blocked from upgrading into them. The release-to-disclosure gap was ≈6 weeks, so the default 7-day hold helps but does not cover the whole exposure window —hold-daysset to 30 or higher would have been decisive. -
OpenSSL "SpookySSL" — CVE-2022-3602 / CVE-2022-3786 (November 2022). Pre-announced as critical, downgraded to high on release; users who upgraded the minute 3.0.7 shipped got a rushed build. cold-brew protection: hold window absorbs the panic-upgrade, and auto-bypass kicks in if the post-disclosure CVSS warrants it. The literal "don't get cut by the bleeding edge" case.
-
Apache Log4Shell — CVE-2021-44228 (December 2021). Critical (CVSS 10.0) RCE in
log4j, reachable throughopenjdkand many Homebrew-installed JVM tools. cold-brew protection: CVSS auto-bypass — the fixed version is promoted out of the hold window immediately. Demonstrates that the hold window does not strand users on vulnerable releases. -
Homebrew-cask PR auto-merge RCE (April 2021, RyotaK disclosure). A researcher demonstrated arbitrary code execution against
homebrew-caskvia auto-merged pull requests. cold-brew protection: hold window — a malicious cask merged at noon does not reach users until the source-file commit ishold-daysold, which is the entire point of the design. -
Sudo "Baron Samedit" — CVE-2021-3156 (January 2021). Heap overflow in
sudo(CVSS 7.8). cold-brew protection: CVSS auto-bypass at the default7.0threshold — fixedsudopromotes fromHeldtoReadyon the first run after the advisory lands.
cold-brew shifts the odds; it does not eliminate the risk. A determined attacker with patience longer than your hold window still gets through. The design assumes most bad releases are caught within days-to-weeks of publication, which the historical record above broadly supports.
- Homebrew
- Bun — runtime and bundler
- Task — task runner used by the build/install recipes
GITHUB_TOKENorGH_TOKENin your environment (optional but strongly recommended). Without one, GHSA lookups are skipped and version-history fetches used for stepping will quickly hit GitHub's unauthenticated rate limit.
task installThis runs task build (Bun bundle + bun build --compile into a standalone
binary at dist/cold-brew) and then sudo ln -sf that binary into
/usr/local/bin/cold-brew.
If you prefer to handle placement yourself:
task build
# dist/cold-brew is a self-contained executable — copy or symlink it anywhere on PATHRunning cold-brew with no arguments is the intended daily command. It runs
brew update, then a cold-brew-aware upgrade, then brew cleanup and brew doctor, filtering out the noisiest output from each step.
cold-brewEvaluates every outdated package and partitions them into ready (hold satisfied or auto-bypassed), held (still inside the hold window), and stepping (held but with a safe intermediate version available). Upgrades the ready set via Homebrew. Restrict to specific packages by name.
cold-brew upgrade # upgrade everything that's ready
cold-brew upgrade curl node # only consider these twoNon-destructive summary of outdated packages, sorted with ready ones first and colored by hold state.
cold-brew statusFull advisory report: every vulnerability and typosquat cold-brew knows about
for your installed packages, grouped by kind and severity. Vulnerabilities
are sorted critical → unknown; typosquats appear separately as informational
entries (they never count toward auto-bypass). --json emits a
machine-readable version for scripting.
cold-brew audit
cold-brew audit --json | jq '.vulnerabilities[] | select(.severity == "critical")'cold-brew hold <pkg> # never auto-upgrade this package
cold-brew release <pkg> # return to the default hold-window behavior
cold-brew allow <pkg> # always upgrade immediately, skipping the hold windowcold-brew config # show current settings
cold-brew config hold-days 10 # set hold window to 10 days
cold-brew config auto-bypass-cvss 8.5 # raise the auto-bypass thresholdReconciles the custom cold-brew/cold-brew tap in tap/ with your local
Homebrew taps directory. The tap currently ships one formula, dogshell.
Launches an interactive terminal dashboard built on @opentui/solid with the Catppuccin Frappé palette. Package list, advisory details, search, and settings screens are all reachable from the keyboard; the status bar at the bottom of the screen shows the active keybindings for whichever view you're on.
Any subcommand cold-brew doesn't recognize is forwarded to brew directly.
brew install and brew reinstall bypass cold-brew's hold policies entirely,
so cold-brew prints a warning when you route them through it. Pass
--no-guard to silence the warning once you've acknowledged it.
cold-brew info curl # just forwarded to `brew info curl`
cold-brew install foo # warning: hold policies do not apply
cold-brew install foo --no-guardSettings live in a SQLite database at ~/.config/cold-brew/cold-brew.db
(managed with Drizzle ORM, WAL mode). The file is
created on first run.
| Setting | Default | Meaning |
|---|---|---|
hold-days |
7 |
Minimum upstream age (days) before a package becomes upgradable. |
auto-bypass-cvss |
7.0 |
CVSS score at or above which a fixable advisory skips the hold window. |
Per-package overrides (always-hold / always-allow / explicit version pin)
are stored in the same database and set via the hold / release / allow
commands.
cold-brew sets HOMEBREW_NO_AUTO_UPDATE=1 on every brew invocation it makes
so its own runs can't be derailed by a mid-command brew update.
To wipe state (cached metadata, advisories, per-package policies, upgrade log):
task db:resetsrc/
├── brew/ Homebrew + advisory core
│ ├── api.ts spawn `brew`, GitHub API, source-date fetching
│ ├── packages.ts discover installed packages and enrich with metadata/advisories
│ ├── policy.ts evaluate Hold.Status for each package
│ ├── stepping.ts pick safe intermediate versions
│ ├── advisories.ts OSV + GHSA + typosquat detection, cached
│ ├── cvss.ts CVSS v3.1 and v4.0 scoring
│ ├── tap.ts custom tap bootstrapping and sync
│ ├── status.ts presentation helpers (icons, colors, sections)
│ ├── validate.ts input validation (package names, GitHub responses)
│ ├── concurrency.ts worker-pool helpers for batched API calls
│ └── types.ts Hold.*, Advisory.*, Package.* type namespaces
├── cli/ command-line surface
│ ├── wrapper.ts `cold-brew upgrade` implementation
│ ├── cycle.ts default maintenance cycle
│ ├── audit.ts `cold-brew audit`
│ ├── status.ts `cold-brew status` helpers
│ └── ansi.ts ANSI color constants
├── db/ SQLite via Drizzle ORM
│ ├── schema.ts config, packageSettings, packageCache,
│ │ metadataCache, advisoryCache, upgradeLog
│ ├── migrate.ts schema migrations
│ └── index.ts typed accessors + defaults
├── tui/ @opentui/solid dashboard (Solid.js in the terminal)
└── index.ts CLI entry point + command dispatch
tap/ the cold-brew/cold-brew Homebrew tap (formula: dogshell)
task dev # run from source with watch mode
task run -- upgrade curl # invoke a subcommand against the live source
task check # tsc --noEmit
task lint # biome check src
task format # biome check --fix src
task test # bun test
task clean # remove node_modules and dist
task db:reset # delete the SQLite databaseLint and formatting are handled by Biome; the config
lives in biome.json. TypeScript path aliases (@brew/*, @cli/*, @db,
@tui/*) are defined in tsconfig.json.
Personal project, no release cadence, no support commitments. Use at your own risk — though that's kind of the whole point.