janpfajfr/vouch
GitHub: janpfajfr/vouch
Stars: 6 | Forks: 0
# vouch

A **dependency-decision ledger** for Node.js projects and coding agents.
**What it is.** `vouch` doesn't decide whether a dependency is safe. It makes sure every
dependency that enters your repo was **recorded, explained, and reviewable in the pull
request** — a memory and conscience for your `package.json`. The record lives in a committed
ledger, keyed by `name@version`, visible in the diff and auditable forever.
**Why use it**
- **Every dependency is a recorded decision** — who added it and why, in a committed ledger
that shows up in the PR diff instead of a lockfile change nobody reads.
- **CI blocks the silent add** — a raw `npm install ` (by a human *or* an agent) can no
longer reach `main` unrecorded.
- **The record stays honest over time** — surfaces a **known CVE** or **version drift** on
something you already recorded, until a human re-decides.
- **Zero dependencies, Node 18+** — a dependency-security tool with none of its own, and it
ships an `AGENTS.md` so coding agents play by the same rules.
**What it is *not***
- **Not a scanner.** Deep per-package analysis (typosquatting, behavioral) is the job of `npq`
and Socket. `vouch` owns **provenance and enforcement**, and flags drift after the fact.
- **Not a replacement for your package manager's install gates.** It *complements* pnpm's
`minimumReleaseAge`, Yarn's age gate, and npm's release-age controls.
- **Not an approval authority.** `vouch` *records* the decision; the *PR review* approves it.
(Each of these is expanded below — see [What it is *not*](#what-it-is-not) and
[vouch records; the PR review approves](#vouch-records-the-pr-review-approves).)

## Contents
- [Quick start](#quick-start)
- [The idea](#the-idea)
- [What you'll see](#what-youll-see)
- [How it works](#how-it-works)
- [vouch records; the PR review approves](#vouch-records-the-pr-review-approves)
- [When `check` blocks on a CVE](#when-check-blocks-on-a-cve)
- [Commands](#commands)
- [Using vouch in a monorepo](#using-vouch-in-a-monorepo)
- [Configuration](#configuration)
- [For coding agents](#for-coding-agents)
- [What it is *not*](#what-it-is-not)
- [Zero dependencies](#zero-dependencies)
## Quick start
**Requirements:** Node.js 18+. No other dependencies.
**Install** — run on demand with `npx`, or install the CLI globally:
npx @vouchjs/vouch --help # no install
npm install -g @vouchjs/vouch # or install the `vouch` command
**Bootstrap the config (optional, recommended)** — one command, never overwrites:
vouch init # writes vouch.config.{mjs,js} with all defaults shown
That gives you a **typed config** (Playwright-style): every option visible with its default,
ready to be edited. Delete the keys you're happy with and vouch will pick up future defaults;
change the ones you're not. `init` also seeds an `AGENTS.md` with the rules for coding agents.
For full editor **autocomplete + type errors** on the generated config, also install vouch as
a dev dependency so its types are reachable from your project:
npm install -D @vouchjs/vouch
`vouch init` detects whether vouch is in your `node_modules` and writes the appropriate
variant — `import { defineConfig } from "@vouchjs/vouch"` when installed, a JSDoc-typed plain export
when not. Either variant **loads at runtime**; only the editor experience differs.
**Add a dependency** — instead of `npm install` / `pnpm add`, run:
vouch some-package # reviews, installs, and records the decision
vouch some-package -D # devDependency
**Gate it in CI** — add one step that fails the build on any unrecorded dependency. Drop this
in `.github/workflows/vouch.yml` (also in [`examples/github-actions-check.yml`](examples/github-actions-check.yml)):
name: vouch
on: [pull_request]
jobs:
vouch:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 20 }
- run: npx @vouchjs/vouch check
That's it. A raw `npm install ` (by a human *or* an agent) can no longer reach `main`
unrecorded. A fresh clone is unaffected — `npm install` / `npm ci` restores the lockfile as
usual, and `check` passes because the committed ledger already covers every dependency.
## The idea
1. **Every dependency is a decision.** Adding one should be recorded — with who and why —
not slipped into a lockfile diff nobody reads.
2. **The record lives in your repo.** Decisions go into a committed ledger
(`.security/dependency-approvals.json`), keyed by `name@version` and wrapped in a
`{ "version": 2, "entries": { … } }` envelope, visible in the PR diff and auditable forever.
Ledgers from 0.1.x (name-keyed) auto-migrate on first read; the updated file appears in
the next mutation's diff.
3. **The record stays honest over time.** When a dependency you recorded later gains a known
advisory, or its version drifts from what was reviewed, CI surfaces it until a human
re-decides.
## What you'll see
A dependency added without `vouch` fails CI:
✦ vouch Dependency review failed
- axios: missing ledger entry
Next — record the unrecorded dependency:
vouch axios
Adding one with `vouch` records the decision (and blocks risky packages until you say why):
✦ vouch Dependency needs review
esbuild@0.28.0
- install-time script detected: postinstall
Next:
vouch esbuild --force-with-reason ""
Once recorded, the ledger entry travels with the diff and CI is green:
✦ vouch Dependency review passed
All dependencies are recorded.
## How it works
**When you add a package** (`vouch `):
1. Reviews it: **version age** and **install-time scripts** (blocked by default).
2. Warns you **right then** if the version already has a **known CVE** — so `check` is never
the first messenger. Configurable: `cveAtInstall: "warn"` (default), `"block"` (refuse to
install advisories at or above `cveAtInstallMinSeverity`, default `"high"`), or `"off"`.
3. Installs it and records the decision (version, risk, who added it and why) in the ledger.
**In CI** (`vouch check`) — three states per dependency: *recorded* (ok), *unrecorded*
(blocked), or *needs review* (blocked). It fails when:
- a direct dependency in `package.json` has **no ledger entry** — added without `vouch`
(covers `dependencies`, `devDependencies`, and `optionalDependencies`; `peerDependencies`
too when `checkPeerDependencies` is enabled);
- a dependency **gained a CVE** that no human has acknowledged;
- a **high-risk** entry has **no `reason`** recorded for the reviewer to judge;
- **version drift** — `check` resolves each dependency's **installed version** from
`node_modules` and asserts that exact `name@version` was reviewed. Run `check` after a
complete install. If installed versions have drifted from what was recorded, run
`vouch @` to re-record them;
- **pinning** — an opt-in `requirePinned` (`"warn"`, default `"off"`) flags deps that use a
range instead of an exact version, suggesting the recorded version to pin to.
## vouch records; the PR review approves
This distinction is the heart of the tool:
- **vouch records a decision.** The ledger entry — who added it (`addedBy`, from `git config`),
why (`reason`), at what version and risk — is *attribution*. It's self-asserted, not by
itself an authorization.
- **The PR/MR review is the authorization.** A human approving the pull request, with the
ledger entry visible in the diff, is the act that approves. vouch makes the decision
conscious and reviewable; it doesn't try to verify or replace that review.
You can always force a thing through with `--force-with-reason`. You can never do it
*invisibly* — the reason and your identity land in the committed ledger, in front of the
reviewer. See [`THREAT_MODEL.md`](THREAT_MODEL.md) for what vouch does and does not
defend.
## When `check` blocks on a CVE
A block isn't damage — it's a pause: *a dependency you recorded carries a CVE no human has signed
off on — either one present when you recorded it (not yet acknowledged), or one that appeared
since (`check` flags it as NEW).* Three honest options, in order of preference:
1. **Fix it** — `vouch @` to record a fixed release.
2. **Remove or replace it** — drop the dependency, or swap in a lighter one.
3. **Accept it knowingly** — once you've judged the risk acceptable (dev-only, unreachable
code path, no fix yet), `vouch acknowledge --reason ""`.
`acknowledge` re-queries advisories for the recorded version and records the acknowledged set,
who acknowledged it (from `git config`), why, and when — visible in the PR diff. It refuses to
write while offline (we never record an acknowledgement we couldn't verify), and it blocks only
on a CVE it **confirmed**: offline or a stalled endpoint fails *open* (a warning, never a failed
build), and only the specific dependency that drifted — never your whole project.
## Commands
| Command | What it does |
|---|---|
| `vouch [-D]` | Review, install, and record a dependency (`-D` for devDependencies). |
| `vouch --force-with-reason ""` | Override a block, recording the reason in the ledger. |
| `vouch check` | CI gate: fail on unrecorded deps, unexplained high-risk, CVE drift, or version drift. |
| `vouch adopt` | Baseline the whole repo: records every installed, unrecorded dependency across all workspaces (deduped by `name@version`). Never installs. Idempotent. |
| `vouch acknowledge --reason ""` | Knowingly accept a dependency's current advisories (CVE drift). |
| `vouch init` | Bootstrap `vouch.config.{mjs,js}` with all defaults shown + detected `packageManager`, and seed `AGENTS.md`. Refuses to overwrite. |
| `vouch --help` · `vouch --version` | Help (with the wordmark) and version. |
Environment: `VOUCH_ADVISORY_URL` overrides the npm advisory endpoint (for enterprise mirrors/proxies).
## Using vouch in a monorepo
vouch is workspace-aware for **pnpm** (`pnpm-workspace.yaml` `packages:`) and **npm/yarn** (`package.json` `workspaces`). It discovers every workspace package, takes the union of their declared dependencies, and operates against **one root ledger** at `/.security/dependency-approvals.json`.
- **Baseline the repo:** run `vouch adopt` at the repo root — it records every installed, unrecorded dependency across all workspaces, deduped by `name@version` (two workspaces on different versions of the same package get two entries).
- **Enforce in CI:** run `vouch check` after installing. It resolves each dependency's **installed** version (from `node_modules`, falling back to `pnpm-lock.yaml` for workspaces with no local `node_modules`) per workspace and fails if that exact `name@version` was never reviewed. Run it after a complete install (`pnpm install --frozen-lockfile`).
- **Add a dependency:** run `vouch ` inside the workspace that needs it. The package manager installs it into that workspace; vouch records it to the **root** ledger at the version installed for that workspace.
Internal dependencies (`workspace:`, `link:`, `file:`, `catalog:`) are skipped — they are intra-repo edges, not external packages. Single-package repos are unaffected beyond the `name@version` keying. Violations are grouped by workspace, and the fix-it list is capped per group with a pointer to `vouch adopt`.
## Configuration
The preferred form is a typed config — `vouch.config.{ts,mjs,js,cjs}` — exporting a
`defineConfig()` call. Every key is optional; the defaults flow through. Generate one with
`vouch init`:
// vouch.config.mjs (or .js if your project has "type": "module" — vouch init picks correctly)
import { defineConfig } from "@vouchjs/vouch";
export default defineConfig({
packageManager: "auto", // "auto" | "pnpm" | "npm" | "yarn"
allowScopedPackages: [],
// Install-time gate
minimumVersionAgeHours: 24,
warnVersionAgeHours: 168,
blockInstallScripts: true,
requireCooldownConfigured: false,
// CI gate — `vouch check`
requirePinned: "off", // "warn" | "off"
checkPeerDependencies: false, // also gate peerDependencies (prod/dev/optional always gated)
// CVE handling at add time
cveAtInstall: "warn", // "warn" | "block" | "off"
cveAtInstallMinSeverity: "high", // "low" | "moderate" | "high" | "critical"
});
Runtime validation still fires (a typo'd enum value fails loudly instead of silently
downgrading a gate).
### Editor types — the install-as-devDep step
To make `import { defineConfig } from "@vouchjs/vouch"` resolve and light up editor autocomplete on the
config, vouch needs to be in your project's `node_modules`:
npm install -D @vouchjs/vouch
`vouch init` detects this automatically and writes the appropriate variant:
| State | Generated config |
|---|---|
| `@vouchjs/vouch` in your `node_modules` | `import { defineConfig } from "@vouchjs/vouch"; export default defineConfig({ ... })` — full editor types via the bundled `.d.ts` |
| `@vouchjs/vouch` not installed locally | `/** @type {import("@vouchjs/vouch").Config} */ export default { ... }` — **no runtime import**, loads anywhere; types light up the moment you `npm install -D @vouchjs/vouch` |
Both variants load at runtime; only the editor experience differs.
### File format
`vouch.config.ts` works on Node 23+ (or 22.6+ with `--experimental-strip-types`). For older
Node, write `.js`/`.mjs`/`.cjs` — vouch loads any of them via dynamic `import()`.
### Package manager detection
### Legacy: `.safe-dep.json`
## For coding agents
`AGENTS.md` tells agents to use `vouch` instead of raw installs, to explain *why* a dependency
is needed before adding it, and — crucially — **not** to silence the gate on a human's behalf.
`vouch init` seeds this file (creating it, or appending a fenced section to an existing one),
so the rules travel with the repo. As agents add more dependencies, the ledger becomes the
place a human reviews those decisions, asynchronously and accountably.
## What it is *not*
Not a scanner. Deep per-package analysis (typosquatting, behavioral) is the job of tools like
`npq` and Socket. We don't *scan* for CVEs to discover them — we record the advisory posture of
what you recorded and flag **drift** after the fact. `vouch` owns **provenance and enforcement**.
Not a replacement for your package manager's native defenses, either. Modern versions ship
install-time gates — pnpm's `minimumReleaseAge` (default on in pnpm 11), Yarn's
`npmMinimalAgeGate`, npm's release-age controls. `vouch` **complements** them: those gate *what*
installs; `vouch` records *who decided, and why*, where the PR can see it — the one thing none of
them do. On package-manager versions without those defaults (e.g. pnpm 9), `vouch`'s install-time
review is the gate you'd otherwise lack.
## Zero dependencies
`"dependencies": {}`. Built on Node 18+ built-ins. A dependency-security tool with no
dependencies of its own.
标签:自动化攻击