{"id":50845347,"url":"https://github.com/sstraus/embargo","last_synced_at":"2026-06-14T09:03:39.435Z","repository":{"id":361409937,"uuid":"1253372804","full_name":"sstraus/embargo","owner":"sstraus","description":null,"archived":false,"fork":false,"pushed_at":"2026-05-30T14:31:04.000Z","size":1141,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-05-30T15:13:15.170Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/sstraus.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-05-29T11:51:42.000Z","updated_at":"2026-05-30T14:31:06.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/sstraus/embargo","commit_stats":null,"previous_names":["sstraus/embargo"],"tags_count":4,"template":false,"template_full_name":null,"purl":"pkg:github/sstraus/embargo","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sstraus%2Fembargo","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sstraus%2Fembargo/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sstraus%2Fembargo/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sstraus%2Fembargo/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/sstraus","download_url":"https://codeload.github.com/sstraus/embargo/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sstraus%2Fembargo/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34315090,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-14T02:00:07.365Z","response_time":62,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-06-14T09:03:38.571Z","updated_at":"2026-06-14T09:03:39.422Z","avatar_url":"https://github.com/sstraus.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003cdiv align=\"center\"\u003e\n\n\u003cimg src=\"assets/logo.png\" alt=\"embargo logo\" width=\"220\"\u003e\n\n# embargo\n\n**Block dependency versions that are \"too fresh.\"**\n\n[![Go Reference](https://pkg.go.dev/badge/github.com/sstraus/embargo.svg)](https://pkg.go.dev/github.com/sstraus/embargo)\n[![Go Report Card](https://goreportcard.com/badge/github.com/sstraus/embargo)](https://goreportcard.com/report/github.com/sstraus/embargo)\n[![CI](https://github.com/sstraus/embargo/actions/workflows/ci.yml/badge.svg)](https://github.com/sstraus/embargo/actions/workflows/ci.yml)\n[![Release](https://img.shields.io/github/v/release/sstraus/embargo?sort=semver)](https://github.com/sstraus/embargo/releases)\n[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)\n[![Go Version](https://img.shields.io/github/go-mod/go-version/sstraus/embargo)](go.mod)\n\n_A supply-chain age gate for npm, cargo, Go, PyPI and friends — no daemon, no service, no telemetry._\n\n\u003c/div\u003e\n\n---\n\n## Why\n\nA package version published **minutes or hours ago** is the highest-risk moment in the\nsoftware supply chain. A compromised maintainer account, a malicious release, or a\ntyposquat is most dangerous *before* the community has had time to notice and yank it.\n\n`embargo` enforces a **minimum release age**: a version must have existed on its registry\nfor at least _N_ hours before you're allowed to install or use it. It's a cheap, boring,\neffective heuristic that buys the ecosystem time to catch bad releases — and buys *you*\ntime to not be patient zero.\n\n```\nBLOCKED  npm  shiny-new-lib@2.0.0\npublished 2h 46m ago — required minimum 3d\n```\n\n## What you get\n\n- **CI gate** — scan lockfiles and fail the build on anything too young. No setup beyond a binary.\n- **Install interceptor** — shims that block risky installs *before* they run (so a malicious `postinstall` never fires), for humans **and** AI coding agents.\n- **Eight ecosystems** — npm, pnpm, yarn, bun, deno *(npm registry)*, cargo *(crates.io)*, go *(module proxy)*, pip / uv *(PyPI)*.\n- **Honest output** — human-readable, JSON, or SARIF for GitHub code scanning.\n- **Self-contained** — one static binary. No daemon, no remote service, no telemetry.\n\n## Platform support\n\n| Mode | macOS | Linux | Windows |\n| --- | :---: | :---: | :---: |\n| `embargo check` (CI gate) | ✅ | ✅ | ✅ |\n| Shim / proxy / `run` (install interception) | ✅ | ✅ | ✅ |\n\n\u003e **Windows:** fully supported. `install-shims` writes `.cmd` and `.ps1` wrappers\n\u003e resolved via `PATHEXT`, and binary resolution honors Windows extensions, so the\n\u003e proxy intercepts `npm`, `cargo`, `go`, `pip` \u0026 co. from both `cmd.exe` and\n\u003e PowerShell. Go-ecosystem checks additionally use the local `go` toolchain when\n\u003e present, falling back to parsing `go.mod` directly.\n\n---\n\n## Install\n\n**Homebrew** (macOS \u0026 Linux):\n\n```sh\nbrew install sstraus/tap/embargo\n```\n\n**Go toolchain:**\n\n```sh\ngo install github.com/sstraus/embargo/cmd/embargo@latest\n```\n\nOr grab a prebuilt binary from the [releases page](https://github.com/sstraus/embargo/releases).\n\n---\n\n## Quickstart\n\nScaffold a config and install the shims in one step:\n\n```sh\nembargo init\n```\n\nThis writes a minimal `.embargo.yaml` (see [Configuration](#configuration)) if one is not\nalready present, installs the package-manager shims, and prints the PATH line to activate them.\nThen scan your lockfiles:\n\n```sh\nembargo check\n```\n\n```\nBLOCKED\necosystem: npm\npackage: shiny-new-lib\nversion: 2.0.0\npublished: 2026-05-29T09:14:02Z\nage: 2h 46m\nrequired minimum: 3d\nsource: package-lock.json\nreason: age 2h 46m \u003c required minimum 3d\n\nFAILED: 1 blocked, 12 allowed (13 checked)\n```\n\n**Exit codes:** `0` = all allowed · `1` = at least one blocked · `2` = internal or config error.\n\n---\n\n## CI usage\n\n`embargo check` is useful on its own — no shims required. It emits SARIF for code scanning\nand JSON for custom tooling.\n\n```yaml\n# .github/workflows/embargo.yml\nname: embargo\non: [push, pull_request]\njobs:\n  age-gate:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n      - uses: actions/setup-go@v5\n        with: { go-version: stable }\n      - run: go install github.com/sstraus/embargo/cmd/embargo@latest\n      - run: embargo check --sarif \u003e embargo.sarif\n      - uses: github/codeql-action/upload-sarif@v3\n        if: always()\n        with: { sarif_file: embargo.sarif }\n```\n\n---\n\n## Intercepting installs (humans \u0026 AI agents)\n\nInstall the shims and put them first on your PATH (`embargo init` runs `install-shims` for\nyou as part of setup):\n\n```sh\nembargo install-shims\nexport PATH=\"$HOME/.embargo/bin:$PATH\"   # add to your shell profile\nembargo doctor                            # verify it took effect\n```\n\nOn Windows, add the shim directory to the front of PATH instead:\n\n```powershell\n# PowerShell\n$env:PATH = \"$HOME\\.embargo\\bin;\" + $env:PATH   # add to your profile\nembargo doctor\n```\n\nNow `npm install`, `pnpm add foo@1.2.3`, `cargo add`, `go get`, `pip install`, etc. are\nrouted through `embargo proxy`, which enforces the age policy. Where possible it blocks\n**before** the real tool runs, so a malicious `postinstall` never executes:\n\n| Command shape | Enforcement |\n| --- | --- |\n| `npm ci`, `npm install` (from lockfile) | **Preflight** the lockfile, block before install |\n| `npm add foo@1.2.3`, `cargo add`, `go get pkg@v1.2.3` | **Preflight** the pinned spec |\n| floating ranges / no lockfile yet | Run, then **re-check** the resulting lockfile |\n| `npm view`, `pip list` (read-only) | Pass through untouched |\n\n### AI coding agents\n\nAI agents that run shell commands inherit your PATH, so the shims apply automatically once\ninstalled. To scope it to a single session without touching your profile, launch the agent\nthrough `run`:\n\n```sh\nembargo run -- claude        # Claude Code\nembargo run -- codex         # Codex CLI\n```\n\n`run` prepends `~/.embargo/bin` to the PATH of the child process and everything it spawns.\n\nAn agent can confirm whether interception is actually active with machine-readable output:\n\n```sh\nembargo doctor --json   # {\"status\":\"active|inactive\",\"reason\":...,\"remediation\":...,\"tools\":[...]}\n```\n\nGate a session on it so the agent refuses to install anything while unprotected:\n\n```sh\ntest \"$(embargo doctor --json | jq -r .status)\" = active \\\n  || { echo \"embargo not protecting — run 'embargo init' and fix PATH\"; exit 1; }\n```\n\nThe `status` field is the unambiguous signal; `remediation` carries the exact command to fix an\n`inactive` state.\n\n### rtk\n\nIf you use [rtk](https://github.com/) (the Claude Code PreToolUse output-filter that\nprepends `rtk` to commands), it composes cleanly: rtk is the outer wrapper, embargo's shim\nis inner, and the argv shape is preserved — so embargo classifies the same command. No\nconfiguration coupling.\n\n---\n\n## Configuration\n\n`embargo` builds its effective policy by layering configs, **lowest precedence first**:\n\n1. **Built-in defaults** — 72h minimum age, all ecosystems enforced, fail-closed.\n2. **Global config** — `~/.embargo/config.yaml`, a baseline applied to every project. Handy\n   when the shim/proxy intercepts commands in directories with no local config.\n3. **Local config** — `.embargo.yaml` in the repo root, or the file given to `--config`.\n\nEach layer overrides only the fields it sets, so the global config is a baseline that local\nconfigs refine (e.g. a global `minimumReleaseAge: 120h` still applies unless a project lowers\nit). `--config` replaces the *local* layer but still sits on top of the global one. Run\n`embargo doctor` to see which config files are active and the effective minimum age. See\n[`.embargo.example.yaml`](.embargo.example.yaml) in this repo for a fully commented example\n(the same schema works as the global config).\n\n```yaml\nminimumReleaseAge: 72h\necosystems: { npm: true, cargo: true, go: true, pip: true } # absent =\u003e all enforced\nallow:\n  packages: [\"@company/*\", \"github.com/company/*\"]\nblock:\n  packages: [\"evil-*\", \"left-pad\"] # denied outright, regardless of age\nremote:\n  lists: [\"https://raw.githubusercontent.com/company/embargo-lists/main/blocklist.yaml\"]\nexceptions:\n  - { ecosystem: npm, package: lodash, version: 4.17.21, reason: \"sec fix\", expires: \"2026-06-15\" }\npolicy:\n  enforceDirectDependencies: true\n  enforceTransitiveDependencies: true\n  failOpenOnRegistryError: false\n  cacheTTL: 24h\n```\n\n### Block list and shared remote lists\n\n`block.packages` is the mirror image of `allow.packages`: any dependency whose name matches a\nblock glob is denied **regardless of age**. A block beats the allowlist, so you can allow a\nwhole scope (`@company/*`) while still denying one compromised package within it. An explicit\n`exception` still wins over a block — that's the deliberate escape hatch for an urgent fix.\n\n`remote.lists` points embargo at **HTTPS** URLs that publish a shared allow/block list — for\nexample a file in a GitHub repo that several teams subscribe to. Each list is fetched, cached\non disk (using `cacheTTL`), and its `allow`/`block` packages merged into the local lists. URLs\nmust use `https`, and a redirect that would leave `https` is refused — a block list's integrity\nis load-bearing, so the transport is never downgraded. The remote file uses the same schema,\nwith just the two list keys:\n\n```yaml\n# blocklist.yaml, published anywhere reachable over HTTPS\nallow:\n  packages: [\"@trusted/*\"]\nblock:\n  packages: [\"evil-pkg\", \"left-pad\"]\n```\n\nA list entry may be a bare URL string or a `{url, required}` mapping:\n\n```yaml\nremote:\n  lists:\n    - https://example.com/blocklist.yaml          # optional (default)\n    - url: https://example.com/critical.yaml       # required: fails closed\n      required: true\n```\n\nBy default, if a remote list is unreachable (network down, 404), embargo **warns and falls back\nto the last cached copy**, or skips that list if it was never fetched — it never fails the run on\na remote hiccup. Mark a source `required: true` to invert that for a security-critical list: if it\nyields no usable content (unreachable with no cache, or malformed) the run **fails closed** (exit\n2). `--no-cache` disables the on-disk fallback and forces a fresh fetch each time.\n\n### How a decision is made\n\nEach dependency runs through an ordered rule chain; the first rule with a verdict wins:\n\n1. **scope** — skip deps whose scope (direct vs. transitive) isn't being enforced.\n2. **exception** — an explicit, expiring allowance for a specific version (overrides a block).\n3. **blocklist** — trusted-deny package globs; a match is blocked outright.\n4. **allowlist** — trusted package-name globs (does NOT override the blocklist).\n5. **age gate** — allow iff `now − publishedAt ≥ minimumReleaseAge`.\n\n### Flags\n\n| Flag | Effect |\n| --- | --- |\n| `--config \u003cpath\u003e` | Path to the policy file (default `\u003croot\u003e/.embargo.yaml`) |\n| `--root \u003cdir\u003e` | Repository root to scan (default `.`) |\n| `--min-age \u003cdur\u003e` | Override `minimumReleaseAge`, e.g. `72h` |\n| `--json` | Emit JSON |\n| `--sarif` | Emit SARIF 2.1.0 |\n| `--fail-open` | Allow on registry errors (default: fail-closed) |\n| `--no-cache` | Bypass the on-disk metadata cache |\n\n---\n\n## Honest limitations\n\nPATH shims are **best-effort**, not a sandbox. They do **not** intercept:\n\n- absolute-path invocations (`/usr/local/bin/npm install`),\n- `python -m pip`, `npx` / `bunx`, `corepack`, `go run`,\n- shell aliases that point straight at the real binary.\n\n`embargo doctor` reports these coverage gaps and whether the shim directory is actually\nahead of the real tools in your PATH. For an authoritative gate, run `embargo check` in\nCI — it reads lockfiles directly and doesn't depend on PATH.\n\nAge is a heuristic, not a guarantee: it buys time for the community to detect and yank bad\nreleases, but it cannot vouch for a version's contents.\n\n---\n\n## Development\n\n```sh\ngo build ./...\ngo test ./...\ngo vet ./...\n```\n\n## License\n\n[MIT](LICENSE)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsstraus%2Fembargo","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fsstraus%2Fembargo","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsstraus%2Fembargo/lists"}