pi-vetter
Security vetting for Pi extension packages — evidence-driven evaluation before install/update
Package details
Install pi-vetter from npm and Pi will load the resources declared by the package manifest.
$ pi install npm:pi-vetter- Package
pi-vetter- Version
0.4.1- Published
- Sep 2, 2026
- Downloads
- 895/mo · 106/wk
- Author
- jesse.t
- License
- MIT
- Types
- extension
- Size
- 156.2 KB
- Dependencies
- 3 dependencies · 2 peers
Pi manifest JSON
{
"extensions": [
"src/index.ts"
]
}Security note
Pi packages can execute code and influence agent behavior. Review the source before installing third-party packages.
README
pi-vetter
Security vetting for Pi extension packages — an evidence-driven gate before you install or update.
Pi installs extension packages with full user permissions and runs their postinstall scripts; the built-in updater only tells you that updates exist. pi-vetter evaluates each candidate version against multiple evidence sources and reports an ALLOW / ASK / DENY verdict with the full evidence list, so you decide what to install — then installs exactly the versions you approved, leaving the update policy in your hands.
pi-vetter can only show that no risk signal was found — it can never prove a package is safe. Read the evidence, not just the verdict.
Install
pi install npm:pi-vetter
(requires Node ≥ 22.19.0; no API keys needed — all default scanners are free)
Usage
| Command | What it does |
|---|---|
/vet |
Read-only evaluation. No arguments = all installed packages with available updates; or pass specs: /vet npm:foo npm:bar@1.2.3 |
/vet-install |
Same evaluation, then an interactive multi-select (TUI) or grouped confirms (non-TUI) and installs only what you approve |
Approved packages are installed via pi install npm:<pkg>@<version> — the exact vetted version, guarded by an integrity re-check before every install (TOCTOU). The install registry (npm config get registry, what pi/npm actually resolves through) is compared against the vetting registry (PI_VETTER_NPM_REGISTRY); on divergence the install is skipped with guidance instead of proceeding on an unverifiable chain. After a successful install, the on-disk package files are compared against the scanned tarball's per-file digests: a match is noted as verified, a divergence surfaces a warning with the removal command (detection, not prevention — lifecycle scripts have already run). By default the settings entry is then restored to an unpinned spec (ADR-0003, revised): deciding to keep a package out of pi update --extensions is yours, not the evaluator's — the install result shows the exact pin command if you want it (install.pinOnInstall: true restores the legacy always-pin behaviour). Pinned packages are still evaluated on every /vet and marked as such in the report.
Verdict model
- ALLOW — no risk signal found from enabled scanners (the report carries the qualifier: this does not prove the package is safe)
- ASK — a rule fired (e.g. new lifecycle script, new outbound endpoint, maintainer change) or evidence is incomplete
- DENY — hard evidence of malice/contradiction (OpenSSF malicious-package advisory, provenance conflict, VirusTotal detections)
Fail-closed: if any enabled scanner fails or times out, the verdict is capped at ASK — never a silent ALLOW (ADR-0002). An earned DENY is never downgraded. Verdicts are rule-driven; the 0–100 risk score is display-only (ADR-0001). Every report also anchors the exact artifact it assessed by showing the verified tarball's sha256.
Scanners (Phase 1)
| Layer | Scanner | Source |
|---|---|---|
| L0 | metadata |
npm registry packument: maintainers (local snapshot diff), package age, release cadence, deprecation, downloads |
| L1 | osv |
osv.dev querybatch — covers CVE + GitHub Advisories (GHSA) + OpenSSF malicious packages (MAL-); new dependencies are queried too, at the version npm would actually resolve (highest published in-range version) |
| L1 | provenance |
npm attestations: full sigstore signature-chain verification against a vendored public TrustedRoot, plus declared-repo conflict detection; verified bundles yield provenance:verified. Missing attestations stay informational unless provenance.required is set |
| L2 | static |
pattern scan of code files: credential access, obfuscation, prompt-injection markers, eval family and dynamic module resolution (concatenated / base64-decoded / variable require-import); pre-existing hits are info, new hits are findings; in the install scenario (no baseline) credential/obfuscation hits stay informational while prompt-injection and dynamic code execution remain hard signals |
| L2 | diff |
old-vs-new tarball comparison: new lifecycle scripts, new dependencies, new child_process usage, new outbound endpoints |
| L3 | virustotal |
hash-first lookup, upload on miss (uploads of new files do not consume the daily quota); ≥2 engine detections → DENY. Disabled by default; enable with an API key |
| L3 | socket |
Socket.dev package alerts (gptMalware, installScripts, obfuscatedFile, typosquatting...); high-risk alerts → ASK (socket-flagged). Disabled by default — note the free-tier token carries a per-call quota balance (a purl scan consumes a chunk of it), so expect routine quota-exhaustion (which caps verdicts at ASK) once the balance runs dry |
Optional L3 engines are disabled by default and enabled per API key in the config file. When an enabled engine hits its quota or fails, the verdict is capped at ASK (fail-closed) and the evidence states why.
Rules
Rules map evidence to verdicts and can be toggled individually in the config file (ask.new-lifecycle-script: false etc.). Current DENY rules: malicious-package, provenance-conflict, vt-detections. Current ASK rules: known-vulnerability, new-lifecycle-script, maintainer-change, new-dependency-flagged, new-network-endpoint, new-child-process, credential-access, dynamic-code-execution, transitive-risk, obfuscation, prompt-injection-marker, young-package, rapid-release, deprecated-candidate, provenance-missing (only when provenance.required is on). Rules you disable are disclosed in the report Notes so an ALLOW never masquerades as a full scan.
Configuration
~/.pi/agent/pi-vetter/config.json (created with defaults on first run; see config.example.json). A scanner set to enabled: true without its credentials is not registered — you get a warning notification at command start and a disclosure line in the report Notes (there is no anonymous access; both VirusTotal and Socket free tiers require registering for a key):
{
"scanners": { "osv": { "enabled": true, "timeoutMs": 10000 }, "virustotal": { "enabled": false, "apiKey": "" } },
"rules": { "deny": {}, "ask": { "young-package": true } },
"provenance": { "required": false },
"cache": { "enabled": true, "ttlHours": 24 },
"score": { "weights": {} },
"network": { "timeoutMs": 30000 },
"install": { "pinOnInstall": false }
}
Scan results are cached per scanner + pkg@version + baseline + artifact digests under ~/.pi/agent/pi-vetter/cache/; VirusTotal hash lookups are cached forever. Caching can be disabled entirely.
Environment variables
Optional endpoint overrides (unset = public defaults). Useful for private registries/mirrors:
| Variable | Default | Purpose |
|---|---|---|
PI_VETTER_NPM_REGISTRY |
https://registry.npmjs.org |
npm registry base URL |
PI_VETTER_DOWNLOADS_API |
https://api.npmjs.org/downloads/point/last-month |
downloads-count API base |
PI_VETTER_OSV_API |
https://api.osv.dev |
OSV API base |
PI_VETTER_DATA_DIR |
~/.pi/agent/pi-vetter |
data dir (config, cache, maintainer snapshots) |
PI_VETTER_AGENT_DIR |
~/.pi/agent |
agent dir (settings.json read for the installed-package inventory) |
Caveats
- Approving an install still executes the package's install scripts — Pi does not install with
--ignore-scripts; pi-vetter warns but cannot prevent this. - Deep dependency scanning (
dependencies.*config, on by default, depth 2 / 20 packages) downloads and statically scans transitive dependency tarballs — each dependency resolves to the highest published version inside its declared range (falling back tolatestwhen the range has no plain-semver shape or no in-range version exists, so the scanned tarball can occasionally differ from what npm would install); hits are attributed per dependency: risky labels (credentials, obfuscation, prompt injection, dynamic code) escalate to an ASK-level transitive-risk rule, ordinary Node.js API usage stays informational. - Bootstrap supply chain: pi-vetter's own runtime dependencies (
@sigstore/bundle,@sigstore/verify,tar-stream) are npm packages too and carry the same theoretical poisoning risk as anything it vets — the evaluator cannot lift itself above its own supply chain. Audit its lockfile like any other tool you grant full permissions. - Non-npm sources (git/local) are out of scope for MVP — a no-argument
/vetdiscloses each skipped source in the report notes instead of evaluating it.
Development
npm install
npm run typecheck && npm test && npm run lint
npx vitest run --project e2e # end-to-end suite (local fake registry)
LIVE_E2E=1 npx vitest run --project live # live e2e against the real registry (network)
Design docs: docs/design.md, ADRs in docs/adr/, research in research/.
License
MIT