← Back to npm Lifecycle Script Check
Every fact this tool reports is already in your own package-lock.json or
node_modules -- hasInstallScript: true is a real field npm writes, and
anyone with an editor and an older copy of the lockfile can grep for it and compare. This page names no
product or company: keeping an old lockfile around and diffing it by eye is a manual habit, not a
competing tool.
It genuinely works right after a dependency bump on a small project, checked by someone who remembers to do it. It gets harder to trust the moment the lockfile runs to hundreds of packages, the check has to happen on every merge rather than the one time you thought to look, and a flagged package's actual script still needs reading -- which is exactly what the table below covers.
| Case | Diffing package-lock.json by hand | This tool |
|---|---|---|
| A small project, checked right after a dependency bump | Works fine -- a short lockfile is easy to hold in your head for one check | Works the same way, instantly, with no need to remember to look |
| A lockfile with hundreds of packages | One line flipping to hasInstallScript: true is not visually distinct from the thousands of lines around it in a diff-by-eye | Parses every entry and reports exactly which name changed, every time, regardless of lockfile size |
| Knowing what a flagged script actually does | package-lock.json only ever records the boolean -- reading the text means separately opening that package's package.json inside node_modules | node cli.mjs snapshot reads the real script text from local node_modules directly, and classifies it against known attacker patterns |
A lockfileVersion 1 file | Has no hasInstallScript field at all -- a grep that finds nothing can misread "field absent" as "nothing installs a script" | Detects the missing packages map and says plainly that this format predates the field, instead of reporting a false all-clear |
| Re-checking on every dependency bump or Dependabot merge | Manual, and the first step skipped once nothing looked obviously different | One CLI command against the last committed snapshot; exits non-zero only when something newly gained a script, so it is safe to wire into CI |
package-lock.json's packages map records hasInstallScript as a
single true/false per entry, because the lockfile stores the resolved dependency tree, not each
package's own package.json contents. Spotting that a package now carries the flag tells you
a script exists and nothing about what it does -- piping a download into a shell reads identically to a
native addon compiling itself, until you open the actual file. A hand check that stops at the boolean has
done half the job; this tool's snapshot command reads the script text straight from local
node_modules and flags it against the same shell-pipe, network-call, and obfuscation
patterns the incidents in this venture's README actually used.
A lockfile diff only catches a newly install-scripted package if someone actually opens the previous
commit's copy and compares it, line by line, against the current one -- and a project with a few hundred
dependencies renders that comparison unreadable by eye long before it becomes impossible. That is the
exact window the four incidents in this venture's README used: none of them needed a victim to be
careless, only for the routine "did anything change" check to not happen on that particular
npm install. This tool's diff command does the same comparison in one pass and
exits non-zero only when something newly gained a script, so it is the check that keeps happening after
the novelty of a hand-check wears off.
This tool's risk tags -- pipes-a-download-to-a-shell, makes-a-network-call,
obfuscated-or-encoded -- are a keyword match against patterns real attacks have used, not a
verdict. A flagged script still deserves your own read: a legitimate installer can also fetch a prebuilt
binary or a font file over HTTPS. What a hand check cannot do at scale is decide, every time, which of
several hundred packages is worth that closer read in the first place -- this tool narrows that list for
you; reading the flagged handful is still yours to do.
Finding the field is a grep away. Knowing whether that is new since your last check is not -- grep on one lockfile has no memory of the previous one, so you still have to keep an older copy around and diff the two by eye. That is exactly the step this tool automates, and exactly the step most likely to be skipped after a routine dependency bump that shouldn't have changed anything.
A mid-sized project's package-lock.json commonly runs several thousand lines across several hundred packages. The one line that flips from absent to hasInstallScript: true on a package you have never had reason to look at twice is not visually distinct from the thousands of lines around it -- there is no diff-by-eye reason it would catch your attention over a version bump on the same package.
It tells you a script exists, never what it does -- lockfileVersion 2 and 3 record only the boolean, not the script text, because the lockfile stores the resolved tree rather than each package's own manifest contents. Reading the actual script means opening that one package's package.json inside node_modules by hand, which is a second, separate step the lockfile diff does not do for you.
Then hasInstallScript is not in the file at all -- the field was added in npm 7's lockfileVersion 2. A hand check that greps for the field and finds nothing can misread "the field does not exist in this format" as "no package has an install script," which is the wrong conclusion for exactly the file where it is easiest to reach.
Once this tool flags a package, reading its actual script text yourself is still the right next step -- a keyword match for a shell pipe, a network call, or obfuscated code means "read this one," not "this is malware." The tool narrows several hundred packages down to the handful that changed; deciding whether a specific flagged script is a real problem is still a human judgment.