Skip to content

ahokinson/cold-brew

Repository files navigation

cold-brew

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.

Why cold-brew

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.

How it works

  • Hold window. A package becomes upgradable once its upstream source file is at least hold-days old (default 7). 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-cvss threshold (default 7.0), cold-brew promotes it from Held to Ready so 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.

Glossary

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.

What this would have caught

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 in cold-brew audit. Honest caveat: informational only — they never auto-bypass and they never block a brew 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 axios RAT and the @bitwarden/cli@2026.4.0 typosquat. 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 malicious axios versions were live for roughly three hours before npm pulled them. cold-brew protection: the bitwarden-cli Homebrew formula is the cleanest direct hit — any formula commit bumping it to the trojaned version stays in Held until hold-days elapse, by which point upstream had already yanked. The axios exposure 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 a preinstall hook 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 xz 5.6.0 and 5.6.1; xz ships in homebrew-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-days set 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 through openjdk and 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-cask via auto-merged pull requests. cold-brew protection: hold window — a malicious cask merged at noon does not reach users until the source-file commit is hold-days old, 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 default 7.0 threshold — fixed sudo promotes from Held to Ready on 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.

Requirements

  • Homebrew
  • Bun — runtime and bundler
  • Task — task runner used by the build/install recipes
  • GITHUB_TOKEN or GH_TOKEN in 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.

Installation

task install

This 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 PATH

Usage

Default: maintenance cycle

Running 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-brew

cold-brew upgrade [pkg…]

Evaluates 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 two

cold-brew status

Non-destructive summary of outdated packages, sorted with ready ones first and colored by hold state.

cold-brew status

cold-brew audit [--json]

Full 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")'

Per-package policy

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 window

Global configuration

cold-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 threshold

cold-brew tap sync

Reconciles the custom cold-brew/cold-brew tap in tap/ with your local Homebrew taps directory. The tap currently ships one formula, dogshell.

cold-brew tui (alias: cold-brew dashboard)

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.

Passthrough

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-guard

Configuration

Settings 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:reset

Architecture

src/
├── 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)

Development

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 database

Lint and formatting are handled by Biome; the config lives in biome.json. TypeScript path aliases (@brew/*, @cli/*, @db, @tui/*) are defined in tsconfig.json.

Status

Personal project, no release cadence, no support commitments. Use at your own risk — though that's kind of the whole point.

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors