Ohbin – uv wrapper for installing tools from GitHub
Summary
Ohbin is a Python tool that acts as a uv wrapper for installing GitHub release binaries directly into a project, eliminating the need for hand-rolled wrapper packages. It automates download, SHA256 verification, caching, and execution via a simple declarative configuration in pyproject.toml.
View Cached Full Text
Cached at: 06/05/26, 11:06 AM
prostomarkeloff/ohbin
Source: https://github.com/prostomarkeloff/ohbin
Your project needs ripgrep, or oasdiff, or some Rust linter that ships only as a GitHub release. Python can’t install it. So you either tell every developer “go install it yourself” — and watch versions drift and CI break — or you hand-write a download-and-verify wrapper package, and copy it into every repo, for every tool.
ohbin deletes that. Declare the tool in pyproject.toml; it’s fetched on first use, SHA256-checked against a pinned hash, cached per host, and exec’d. One dev-dependency. Any number of tools.
uv add --dev git+https://github.com/prostomarkeloff/ohbin.git
Before & After
❌ The hand-rolled wrapper — a whole package, per tool, copied into every repo
# a download-and-verify wrapper · ~180 lines · written again for the next tool
_PLATFORM_ASSETS = {
("linux", "x86_64"): _Asset("ripgrep-14.1.1-x86_64-unknown-linux-musl.tar.gz", "4cf9f2741e6c…"),
("darwin", "arm64"): _Asset("ripgrep-14.1.1-aarch64-apple-darwin.tar.gz", "24ad767777…"),
# ...two more, each SHA hand-copied from the release page
}
def ensure_binary() -> Path:
asset = _resolve_asset() # platform.machine() guesswork
with _flock(cache / ".lock"): # concurrency, if you bother
_download(url, archive) # urllib + redirects (+ retries, if you bother)
_verify_checksum(archive, asset.sha256) # hashlib
_extract(archive, binary) # tarfile, atomic rename
return binary
# + a wheel shim, [project.scripts], and a [tool.uv.sources] entry — in every repo
✅ ohbin — one dev-dependency, one table per tool
uv run ohbin add BurntSushi/ripgrep --version 14.1.1 --name rg --binary rg
[tool.ohbin.tools.rg]
repo = "BurntSushi/ripgrep"
version = "14.1.1"
binary = "rg"
# + one [..assets.<os>-<arch>] table per platform — written by `add`, checksums and all
uv run ohbin run rg -- TODO src/
One is a package you maintain. The other is a table you declare.
Why a wrapper at all?
uv can’t install an arbitrary GitHub-release binary — and that’s not an oversight. uv run <name> resolves to a Python console-script entry point, which is static wheel metadata baked at build time. There is no hook that reads a config table and conjures a command. So something has to bridge “a binary on a release page” to “a command in your venv.”
The honest choices are (a) a wrapper package per tool — the duplication above — or (b) one generic engine that reads a manifest. ohbin is (b): the per-tool detail (repo, version, per-platform asset + checksum) lives in [tool.ohbin.tools.*], and a single mostly-stdlib engine does download / verify / cache / exec for all of them.
ohbin add does the boring part
Point it at a repo. It resolves the release, matches one asset per platform, pins each SHA256 (from the GitHub API digest, else by downloading and hashing), and writes it into your pyproject — comments and formatting intact, via tomlkit:
$ uv run ohbin add BurntSushi/ripgrep --version 14.1.1 --name rg --binary rg
resolving BurntSushi/[email protected] ...
+ linux-x86_64 ripgrep-14.1.1-x86_64-unknown-linux-musl.tar.gz (downloaded+hashed)
+ linux-aarch64 ripgrep-14.1.1-aarch64-unknown-linux-gnu.tar.gz (downloaded+hashed)
+ darwin-x86_64 ripgrep-14.1.1-x86_64-apple-darwin.tar.gz (downloaded+hashed)
+ darwin-arm64 ripgrep-14.1.1-aarch64-apple-darwin.tar.gz (downloaded+hashed)
wrote [tool.ohbin.tools.rg] to pyproject.toml
--name sets the command when it differs from the repo (ripgrep → rg); --binary sets the executable’s name inside the archive. Odd naming scheme? The manifest is the source of truth — add just fills it; fix an entry by hand. Uses the gh CLI when present (auth, rate limits), else the public REST API (GH_TOKEN / GITHUB_TOKEN honored).
ohbin run does the rest
uv run ohbin run rg -- --files # first run: download → verify → cache → exec
uv run ohbin run rg -- TODO src/ # next runs: straight to exec
uv run ohbin which fd # print the cached path (downloads if needed)
uv run ohbin list # declared tools + resolved platforms
Every command takes --pyproject-file PATH to target a specific manifest; reads otherwise discover the nearest pyproject.toml with [tool.ohbin] (or $OHBIN_PYPROJECT), while add / add-gist write only to the CWD’s pyproject. Each run prints [ohbin] resolved pyproject as <realpath> so the target is never a guess.
run replaces the process with execv, so the tool owns stdin/stdout, signals, and the exit code — drop-in for CI and Make, where the prefix disappears behind a variable:
RG := uv run ohbin run rg --
search:; $(RG) TODO src/
Private binaries — encrypted, through a gist
add assumes the release is public. But you have a tool you built yourself and don’t want on a public repo — a closed-source linter, a vendored binary, an internal CLI. You still want ohbin run to just work.
The answer is a secret gist carrying the binary encrypted with a password. The gist link is link-gated (unlisted, not searchable); the password lives only in a private repo. A leaked link alone is useless — the bytes are AES garbage without the key, and ohbin is what decrypts them. No TTL, no key server: to revoke, delete the gist or rotate the password.
$ uv run ohbin publish-gist ./dist/mytool --password "$PW"
published mytool (current platform) to https://gist.github.com/you/ab12…
add it with: uv run ohbin add-gist https://gist.github.com/you/ab12…
publish-gist gzips the binary, encrypts it (openssl AES-256-CBC, PBKDF2 / 200k iters), base64s the ciphertext into one gist file per platform, and writes an ohbin.json index next to them. Publish each platform from its own machine — pass --gist <id> to add to the same gist:
uv run ohbin publish-gist ./dist/mytool-linux --password "$PW" --platform linux-x86_64 --gist ab12…
add-gist reads the index, pins each blob’s immutable raw_url + ciphertext SHA256, and writes an encrypted = true tool into pyproject:
$ uv run ohbin add-gist https://gist.github.com/you/ab12… --name mytool
wrote [tool.ohbin.tools.mytool] (encrypted) to pyproject.toml
run it with: uv run ohbin run --password <pw> mytool -- <args>
At run time the password comes from --password (before the tool name — args after it forward to the tool), or from a password field in the manifest. run verifies the downloaded ciphertext SHA, decrypts, checks the plaintext SHA (a wrong password is caught cleanly, not as a crash), then caches and execs like any other tool:
uv run ohbin run --password "$PW" mytool -- --help
Needs the
ghCLI (reuses your auth) andopensslon PATH. The password never touches argv — it’s piped toopensslvia a file descriptor. Store it withadd-gist --passwordonly if committing it is acceptable; otherwise pass--passwordat run time.
How it works
ohbin run rg -- --version
│
├─ read [tool.ohbin.tools.rg] _manifest (walks up to your pyproject)
├─ pick the asset for this os/arch _platform (→ darwin-arm64)
│
├─ cached? ~/.cache/ohbin/rg/14.1.1/rg
│ ├─ yes ───────────────────────────────┐
│ └─ no → flock → download → SHA256 ✓ │ _engine
│ → extract (tar/zip/raw) → +x │
▼ ▼
os.execv(binary, ["rg", "--version"]) ◄──────┘
- Cache —
$XDG_CACHE_HOME/ohbin/<tool>/<version>/<binary>(~/.cache/…default). The version is in the path, so a bump is a clean new download that never collides with the old one. - Concurrency — the first caller downloads under a
flock; the rest wait and reuse. Safe under xdist / parallel CI. - Integrity — SHA256-checked before extraction. A mismatch aborts; nothing partial lands in the cache.
It survives the network
Release assets live behind CDNs that hiccup; gh rate-limits; DNS blips mid-clone. Every release lookup and every download retries with exponential backoff — and a real 404 is never mistaken for a transient failure (the bug that makes naive wrappers cry “release not found” on a dropped packet):
$ uv run ohbin add BurntSushi/ripgrep --version 14.1.1
ohbin: download failed (attempt 1/4): … Connection reset by peer; retrying in 0.5s
+ linux-x86_64 ripgrep-14.1.1-x86_64-unknown-linux-musl.tar.gz (downloaded+hashed)
…
That is a real line from a live run — a reset connection, recovered, no fuss.
In-process
Need the binary’s path, not to exec it? Same manifest, one call:
from ohbin import ensure
path = ensure("rg") # -> pathlib.Path, downloaded + verified on first use
Discovery walks up from CWD to the nearest pyproject.toml carrying [tool.ohbin]; set OHBIN_PYPROJECT to point at a specific file (CI, or callers running from an unrelated directory).
Hand-rolled wrapper vs ohbin
| wrapper package per tool | ohbin | |
|---|---|---|
| Packages to maintain | one per tool | one, total |
| New tool | write a new package | ohbin add |
| New repo | copy the files | one dev-dependency |
| Checksums | hand-pinned from the release page | auto-pinned by add |
| Network resilience | re-implemented (or skipped) | retry + backoff, built in |
| Integrity check | re-implemented per wrapper | shared, SHA256 |
Limitations
- POSIX only. The install lock is
fcntl.flock; the engine importsfcntlat the top, so Windows fails on import. - Four platforms. linux/darwin × x86_64/arm64 are what
addauto-resolves. Others (windows, musl, riscv) you add by hand — the engine runs them fine. - Heuristic matching.
addmatches assets by OS/arch tokens in the filename and prefers.tar.gz. The manifest is the source of truth; an unusual scheme is a one-line fix.
Development
git clone https://github.com/prostomarkeloff/ohbin
cd ohbin && uv sync
make lint-heavy # ruff format + ruff check --fix + pyright
make test-full # 68 network-free tests (platform / matching / manifest / engine / crypto / gist / retry)
CI runs the lint once, then an os: [ubuntu, macos, windows] × python: [3.11, 3.12, 3.13, 3.14] matrix.
Stop copying wrapper packages. Start declaring binaries.
Made with 📦 by @prostomarkeloff
Similar Articles
oven-sh/bun
Bun is an all-in-one toolkit for JavaScript and TypeScript apps, providing a fast runtime, package manager, and test runner as a single executable. It aims to be a drop-in replacement for Node.js with significantly faster startup and lower memory usage.
Ota
Ota is a new product launch on Product Hunt offering contract-first repository readiness infrastructure.
octoscope
Octoscope turns your GitHub profile into a live terminal-style dashboard.
Fully in-browser container builds
A web application that builds containers entirely in the browser using client-side code, demonstrating the power of custom container tooling. Users can pick a base image, run a shell script, and export the resulting image as a tar file.
Building the deployment tool I wish I had
The author details the development of 'Deptool', a custom Python-based deployment and configuration management tool designed to be faster and more predictable than existing solutions like Ansible, driven by a desire for digital sovereignty and better tooling.