π escrow
A supply-chain firewall for your package managers. escrow sits between your developers (or CI) and the public registries β npm, PyPI, Go, Cargo, NuGet, Maven/Gradle, Composer β and won't hand over a package until it has passed your policy: a minimum age, a clean OSV vulnerability record, a known publisher. One static binary, seven ecosystems, a real-time dashboard in the browser and the terminal.
developer / CI β escrow proxy β upstream registry
β
policy engine
ββββββββββββββΌββββββββββββββ
age osv publisher Β· popularity
A package that fails policy is removed from the manifest before the tool ever sees it β not an error a --force can override, just a version that appears not to exist. Blocked events surface in the dashboard, where an operator approves with one click. And because new CVEs land every day, escrow keeps re-scanning what you already pulled and flags anything that turned vulnerable after the fact.
| npm |
PyPI |
Go |
Cargo |
NuGet |
Maven / Gradle |
Composer |
| β
|
β
|
β
|
β
|
β
|
β
|
β
|

Real-time operator console β light/dark, color-blind-safe β and a full terminal UI (escrow-cli tui) for the same views over SSH.
Jump to: Why escrow? Β· Quick install Β· See it in action Β· Documentation
π€ Why escrow?
Most supply-chain attacks don't exploit a clever bug β they ship a brand-new version of a package you already trust, from a hijacked maintainer account, and race to spread before anyone notices. By the time an advisory exists, the malware has been in node_modules for hours.
escrow's core bet is time. A version published today can't be installed through the proxy until it has aged past your threshold (say, 7 days) β long enough for the community, the registry, and the scanners to catch a bad release and pull it. The attacks below were all caught and removed within hours to a couple of days. With a 7-day quarantine, none would have reached a developer installing through escrow:
| Incident |
What happened |
Closed by |
| ua-parser-js Β· Oct 2021 |
Hijacked maintainer account published 0.7.29 / 0.8.0 / 1.0.0 with a cryptominer + credential stealer β live on npm for ~4 hours |
ποΈ age gate |
| coa & rc Β· Nov 2021 |
Same playbook days later: account takeover, malicious new versions, pulled within hours |
ποΈ age gate |
| colors & faker Β· Jan 2022 |
Maintainer self-sabotage shipped an infinite-loop colors@1.4.44-liberty-2, breaking thousands of builds overnight |
ποΈ age gate |
| Log4Shell Β· CVE-2021-44228 |
A known critical CVE in log4j-core (Maven) β not new, but catastrophic |
π OSV scan + π re-scan |
The age gate closes the zero-day-malware-in-a-fresh-version window; the OSV scan and continuous re-scan close the known-CVE window β including CVEs disclosed after you downloaded a package. escrow is also deliberately honest about what it can't stop (postinstall hooks, typosquatting, git deps) β see Security model & threat coverage.
This is not "catches everything." It's a focused, layered gate over the most common and most time-sensitive attack window β and it tells you plainly where it doesn't reach.
π Quick install
πΊ Homebrew (macOS β recommended)
brew tap jverhoeks/tap
brew install escrow
brew services start escrow # background service, auto-starts on login
# β http://localhost:7888/dashboard
# credentials are printed to: $(brew --prefix)/var/log/escrow.log
Config lives at $(brew --prefix)/etc/escrow/escrow.toml; brew services restart escrow to reload. The formula also installs escrow-cli β the companion that routes your dev environment through the proxy, watches activity, and reloads config. See Routing traffic to escrow.
π³ Docker
docker run -p 7888:7888 ghcr.io/jverhoeks/escrow:latest
# or, with a full debug config (all 7 ecosystems, admin / escrow):
cd docker/ && mkdir -p data && cp escrow.debug.toml data/escrow.toml && docker compose up -d
π¦ Binary
# pick your platform: darwin-arm64 Β· darwin-amd64 Β· linux-amd64
curl -L https://github.com/jverhoeks/escrow/releases/latest/download/escrow-darwin-arm64 -o escrow
chmod +x escrow && ./escrow # binds 127.0.0.1:7888 (localhost only)
./escrow --host=0.0.0.0 # listen on all interfaces (team/CI use)
On first boot escrow generates escrow.toml with a random dashboard password and prints the credentials to stdout β save them.
π Supported ecosystems
| Ecosystem |
Tools |
Proxy URL |
Config key |
| npm |
npm, pnpm, yarn, bun |
http://localhost:7888/ |
npm = true |
| PyPI |
pip, uv |
http://localhost:7888/pypi/simple/ |
pypi = true |
| Go modules |
go |
http://localhost:7888/go/ |
go = true |
| Cargo |
cargo |
http://localhost:7888/cargo/ |
cargo = true |
| Composer |
composer |
http://localhost:7888/composer/ |
composer = true |
| NuGet |
dotnet, nuget |
http://localhost:7888/nuget/index.json |
nuget = true |
| Maven / Gradle |
mvn, gradle |
http://localhost:7888/maven2/ |
maven = true |
β Step-by-step setup for each tool: per-tool quickstarts.
πΈ See it in action
A real-time operator console β light/dark, color-blind-safe (icons and color), with shared
Activity and Ecosystem filters β plus a full terminal UI for the same views over SSH.
| CVEs blocked by advisory |
Package tree (downloaded + flagged) |
Terminal UI β live feed |
 |
 |
 |
β Full visual tour: dashboard & terminal UI β
π Documentation
β‘ GitHub Actions
Use escrow as a one-step supply-chain gate in any CI pipeline β add it before your install steps, no other changes needed:
steps:
- uses: actions/checkout@v6
- uses: jverhoeks/escrow@v1
with:
ecosystems: 'npm'
min-days: '7'
osv-severity: 'HIGH'
- uses: actions/setup-node@v6
with: { node-version: '20' }
- run: npm install --ignore-scripts # automatically uses the escrow registry
Escrow exports NPM_CONFIG_REGISTRY, PIP_INDEX_URL, GOPROXY, etc. so every install routes through the proxy, and caches packages in the Actions cache between runs.
β Full guide, inputs/outputs, caching, and Renovate composition: docs/github-actions.md.
π¨ Building from source
git clone https://github.com/jverhoeks/escrow
cd escrow
go build -o escrow ./cmd/escrow # proxy server
go build -o escrow-cli ./cmd/escrow-cli # companion CLI (macOS / Linux)
go test ./...
π License
MIT Β© 2026 Jacob Verhoeks