npryx
v1.0.0
Published
A security-first superset of npx: previews install-script, provenance, age, popularity and typosquat signals before running a remote package, fails closed, and remembers approvals.
Maintainers
Readme
npx will happily download and execute an arbitrary remote package behind a
bare prompt:
Need to install the following packages:
[email protected]
Ok to proceed? (y/N)That prompt tells you nothing: not whether the package runs install scripts,
how old or popular it is, whether it's deprecated, whether it has build
provenance, or whether the name is one keystroke away from a package you actually
meant. It's exactly where supply-chain attacks land: typosquats, postinstall
RCE, hijacked re-publishes.
npryx hands your command to the real npx, but first it shows you the
trust signals npx hides, fails closed if it can't verify the package, offers
a one-keystroke --ignore-scripts safe run, and remembers what you've already
approved so the prompt keeps meaning something. It then runs exactly the
version it showed you, never a fresh resolve.
npryx: about to fetch & run a package from the npm registry
! [email protected]: 2 warnings, review before running
- runs install scripts (postinstall) which execute code on install
- published only 11d ago, brand new with little scrutiny yet
package [email protected] (asked: latest)
published 11d ago
weekly dl 41,284,663
maintainers esbuild
repo git+https://github.com/evanw/esbuild.git
integrity sha512-HrJrvZv5ayxBzPfwp…
provenance ✓ https://slsa.dev/provenance/v1
install hook ! yes, runs code on install
[y] run [s] run with --ignore-scripts (safer) [a] always-trust this version [N] abort (default):In a terminal the marks and the verdict are coloured (green ✓, yellow !, red ✗ for
tampered bytes or a confirmed threat). Piped or logged output stays plain. NO_COLOR=1
turns colour off and FORCE_COLOR=1 turns it on. On a narrow terminal the choices
stack one per line.
What it shows
A single npm view <spec> --json (so it resolves ranges, tags, and your
.npmrc, including private registries and auth) gives every signal below.
Registry flags you pass (--registry, --userconfig, --@scope:registry, …) are
used for the preview too, so it reads the same registry the run installs from.
Weekly downloads come from the public npm API as a best-effort hint, and only for
packages served by the public registry, so private package names never leave your
machine.
| signal | flagged when |
|---|---|
| install scripts | preinstall / install / postinstall present: the package runs code the moment it's installed |
| provenance | shown ✓ when the build has signed SLSA provenance attestations |
| publish age | younger than ~30 days: brand new, little scrutiny yet |
| weekly downloads | under ~1,000: unusually low |
| deprecation | the maintainer marked it deprecated |
| typosquat | the name is one edit or one swapped pair of letters away from a popular package (crossenv → cross-env, lodahs → lodash) |
| context | resolved name@version, maintainers, repo, and dist.integrity, always shown |
Scope: npryx previews the target package:
npm viewreports the target's own install scripts, not a transitive dependency's. Thes(--ignore-scripts) option blocks install hooks across the whole tree, so reach for it when a target you trust pulls deps you don't.
The prompt
| key | action |
|---|---|
| y | run it (npx --yes <args>), pinned to the version shown |
| s | run with --ignore-scripts (skips install hooks; safer). Note: a few packages legitimately need a postinstall to fetch a native binary, e.g. esbuild. If it breaks, re-run with y. |
| a | always-trust this exact version, then run (see below). Not offered when the package couldn't be verified. |
| N | abort (the default: just press Enter) |
Trust model (TOFU)
Choosing a records the version and its integrity hash (sha512) in
~/.npryx.json. You can trust several versions of the same package. Next time:
- same version, same integrity → the bytes you approved. npryx prints
✓ [email protected] is trustedand runs with no prompt. No fatigue, no reflexivey. - a version you haven't approved → a normal new release. npryx shows the full
preview with a note (
you trusted [email protected]; this is 1.6.0) and asks again. - same version, different integrity → npm never lets a published version be
overwritten, so a registry, mirror or proxy is serving altered code. npryx shows
a red
✗ do not run, and this is never auto-run, not even withNPRYX_YES=1.
npryx --trust-list # show what you've trusted
npryx --forget <pkg>[@<version>] # drop a package, or one version of itFails closed
If npryx can't verify a package (no such name, registry error), it does not quietly run it:
- interactive terminal → it warns and defaults the prompt to No.
- non-interactive / CI → it refuses and exits 1 for any package that isn't
already trusted. This deliberately inverts npm's "assume yes in CI" default:
an unverified package never auto-runs in a pipeline. To allow packages, set
NPRYX_ALLOWto a comma-separated list ofname(any version),name@version, an integrity hash (sha512-…) orname@version#sha512-…(exactly those bytes, the strictest).NPRYX_YES=1, or-yplaced before the package, opts out entirely. A-yafter the package belongs to the command being run and changes nothing.
It also refuses, rather than guesses, when it can't tell which package would run:
- unrecognised flags before the package. npm treats any config key as a flag,
so an unknown
--flagmight swallow the next argument. Write it as--flag=valueand npryx will pass it through. - git, URL and alias specs (
github:u/r,git+ssh://…,https://…/x.tgz,foo@npm:bar) can't be checked against the registry, so they're treated like an unverifiable package: a prompt that defaults to No, or a refusal in CI unless the exact spec is inNPRYX_ALLOW.
Every -p package is previewed, not just the first.
Two things go straight to npx without a preview:
- local paths (
./x,/abs/path,file:…,x.tgz): your own files. - bins installed in your project (
npx tscwithtypescriptinnode_modules), which npx runs without fetching anything. These are passed with--no, so if npryx misjudged and npx would need to install something, npx refuses instead.
Remote scanning (optional)
npryx can also ask a scan service whether a package sends data off your machine: environment variables to a raw IP, tokens to a webhook, secrets hidden in DNS lookups. The service reads the code, compares it with the previous version, checks OSV for known malware and, if you opt in to that too, runs the package in a sandbox full of fake credentials to see if any of them try to leave.
It's off by default. Nothing is sent anywhere until you turn it on:
npryx --scan-config https://scan.example.com [--token <key>] [--deep]
npryx --scan-status
npryx --scan-off- Only packages from the public npm registry are sent. Private package names never leave your machine.
- Results are signed, and npryx pins the service's key when you opt in.
- A scan can only add caution. A confirmed threat is blocked like tampered bytes:
never auto-run, even with
NPRYX_YES. If the service is down, slow or unverifiable, npryx behaves exactly as it does without it. - The service is open source (
scan-service/, Rust), so you can run your own. A hosted version needs an API key.
Design and detection details: docs/scan-service.md. Wire format: docs/scan-api.md.
For agents and scripts
npryx --json <pkg> [args...] checks a command without running it. It never
prompts, never runs anything and never writes the trust store. stdout is a single
JSON document and stderr stays empty. --json must come first. The decision is
the same one a non-interactive npryx <args> acts on, so a loop looks like this:
- Check:
npryx --json cowsay moo allow(exit 0): runnpryx <command.npryx…>. Without a terminal it runs only if the decision is stillallow, pinned to the version shown.needs-approval(exit 3): show a human the document. If they approve, runNPRYX_ALLOW='<approve>' npryx <command.npryx…>.approveisname@version#integrity, so it allows exactly the previewed bytes: not another version, not a re-published tarball.refuse(exit 1): don't run it. Tampered bytes and confirmed threats can't be approved by anything. Anunverifiablepackage (lookup failed, git or URL spec) needs a human to check the spec themselves.
| exit | meaning |
|---|---|
| 0 | allow: trusted, a local path, or allowed by NPRYX_ALLOW / NPRYX_YES |
| 3 | needs-approval: verified but not trusted, a human should decide |
| 1 | refuse: tampered, confirmed threat or unverifiable, or an internal error |
| 2 | usage error (unrecognised flag, no package, a command with no JSON mode) |
{
"schemaVersion": 1,
"decision": "needs-approval",
"reason": "untrusted",
"message": "not trusted yet: a human should review the packages, then approve them",
"command": { "npx": ["--yes", "[email protected]", "moo"], "npryx": ["[email protected]", "moo"] },
"approve": "[email protected]#sha512-…",
"packages": [{
"requested": "cowsay", "kind": "registry", "decision": "needs-approval", "reason": "untrusted",
"resolved": "[email protected]", "name": "cowsay", "version": "1.6.0", "integrity": "sha512-…",
"provenance": null, "installScripts": { "runs": false, "hooks": [] },
"published": "2024-07-08T15:31:44.000Z", "weeklyDownloads": 412345,
"maintainers": ["piuccio"], "repo": "git+https://github.com/piuccio/cowsay.git",
"deprecated": null, "typosquatOf": null, "publicRegistry": true,
"warnings": [],
"trust": { "status": "updated", "approvedAt": null, "trustedVersions": ["1.5.0"] },
"scan": null,
"approve": "[email protected]#sha512-…",
"error": null
}]
}reason:trusted,local,allowed,yes,nothing-to-install,local-bin(allow);untrusted(needs-approval);tampered,confirmed-threat,unverifiable(refuse). The top level takes the least clear package's.warnings[].code:install-scripts,deprecated,new-package,low-downloads,typosquat,npm-subcommand.trust.status:new,updated,trustedortampered(nullif unverified).scan:nullunless remote scanning is on, elsestatus(done,pending,integrity-mismatch,unavailable),verdict,signed,previousandfindings.commandisnullwhen the decision isrefuse. Errors are{ "schemaVersion": 1, "error": { "code": "usage" | "internal", "message": "…" } }.npryx --trust-list --jsonandnpryx --scan-status --jsonwork too.
New fields may be added within a schemaVersion; a field changing meaning or going
away bumps it.
Approval should come from a human. npryx can't tell who set NPRYX_ALLOW, so if your
agent's harness asks before running commands, keep it asking for ones that set
NPRYX_ALLOW or NPRYX_YES. There is deliberately no way to write the trust store
without a terminal.
Install
Requires Node 22+. Zero runtime dependencies. The only dev dependency is
Hegel for property-based tests,
pinned by the lockfile and installed with scripts disabled (see .npmrc).
npm install -g npryx
npryx cowsay "moo"Want every npx to go through npryx? That's optional. You can keep typing npryx
when you want the check, or alias npx to it:
npryx --setup-alias # shows the exact lines and the file, then asks before adding them
npryx --remove-alias # takes out exactly that block again
npryx --alias # just prints the line, if you'd rather edit your config yourself--setup-alias supports zsh, bash and fish. It adds one clearly marked block, does
nothing if it's already there, and won't touch an npx alias you've defined yourself.
With the alias in place, command npx … still runs the real npx for a single command.
Prior art
npq pioneered install-time marshalling of npm
package safety signals, and npryx borrows its signal taxonomy. npryx differs by
wrapping npx execution specifically, failing closed, offering the inline
--ignore-scripts run, and keeping a TOFU trust store.
Roadmap
- Hosted scan service. The scan service (
scan-service/) runs today if you host it yourself. A hosted version, so you can opt in without running anything, is next. - Transitive install-script preview. Today npryx previews the target only, but
the real risk is often a dependency's
postinstall. A metadata-only resolve (npm install --package-lock-only --ignore-scripts: no download, nothing executed) surfaces every package in the tree that runs install scripts. The trust store would pin that set, so a trusted target re-prompts when its tree changes. - Local static scan. Compile the scan service's analyzer to WebAssembly and ship it in the npm package, so the "does this send data somewhere" check can run on your own machine with nothing sent anywhere.
npryx is published from CI with provenance (.github/workflows/release.yml), so
npryx npryx shows its own provenance ✓.
License
ISC. See LICENSE.md.
