Skip to content

binpatch

Every CLI update re-downloads the whole binary. Most of it never changed. Patch only what moved.

You ship a 100 MB binary. A bug fix lands. The user runs mycli update and pulls another 100 MB — even though the fix touched a few hundred kilobytes. Repeat that across every release and every machine, and you are burning bandwidth and patience for bytes that haven’t moved since the last build.

The old binary and the new one are almost identical. A binary delta (compressed bsdiff) captures just the difference: typically 0.05–0.1% of the full size. That 100 MB update becomes a ~190 KB patch.

A delta patch is useless without both:

  1. Generation — produce the patch from old → new in CI, and publish it somewhere your users can find it.
  2. Application — discover the right patch(es) for a user’s installed version, download them, and reconstruct the new binary safely (integrity checks, size caps, progress).

And if the user is several versions behind, they don’t get a single patch — they get a chain of patches (1.2.0 → 1.2.1 → 1.2.2 → 1.3.4). binpatch chains them automatically, downloads them in parallel, applies each hop in order, verifies the cumulative SHA-256, and falls back to a full download if any hop is missing or malformed.

Most projects hand-roll one half and skip the other. binpatch gives you both, as a small MIT-licensed TypeScript library plus a drop-in GitHub Action.

Generate from CI

The binpatch/generate GitHub Action shells out to a pinned bsdiff, produces one patch per platform, and publishes them to GHCR or GitHub Releases — automatically, on every release.

Apply in your binary

resolveAndApply() discovers the patch chain for the user’s installed version, downloads only what’s needed, and reconstructs the new binary with SHA-256 verification and an OOM guard built in.

Battle-tested at scale

Powers self-updates in production for shipped CLI binaries you may already be using. Same reliability you’d build into your own tool — minus the years of accumulated fixes.

Pure Node, tiny

No native bindings, no WASM, no shelled-out processes at apply time. One ESM package, zero runtime dependencies beyond Node 22+.

CI user's machine
────── ───────────────
old binary (100 MB)
┌──────────────────────┐
│ binpatch Action (CI) │ new binary (100 MB)
│ bsdiff old → new │ ──────────────────────►
└──────────────────────┘
patch (70 KB) ────────────────────────► ┌──────────────────────────┐
│ resolveAndApply() │
│ download · verify · │
│ reconstruct new binary │
└──────────────────────────┘
installed binary (100 MB)

The user downloads kilobytes instead of megabytes. Your CI does the heavy lifting once.

  • You ship a self-updating CLI or agent binary (the standard mycli update story).
  • Your binary is big enough that deltas pay off — generally >50 MB.
  • You want generation and application handled, not two half-solutions.

Do not reach for it when updates are source-level (use git, npm update), or when your binary is tiny (deltas only amortize at scale).

Terminal window
npm install binpatch

Then both: generate patches from CI with the GitHub Action (so nightly builds push to GHCR and stable releases push to GitHub Releases), and wire resolveAndApply into your binary’s update command to discover and apply them.

import { resolveAndApply } from "binpatch";
const result = await resolveAndApply({
currentVersion: "1.2.0",
targetVersion: "1.3.4",
source: ghcrSource({ repo: "myorg/mycli" }),
});
// result.destPath now holds the verified 1.3.4 binary; download was ~70 KB.