To diff a suspicious release, download the old and new versions without installing them, unpack each in a throwaway directory, and compare the files. Look for new install scripts, new network calls and files that your last review never saw. Do this in a disposable environment.
Set up a safe place first
You are about to handle code someone says is bad. Use a disposable container or virtual machine with no credentials, no mounted home directory and no access to internal networks. Do not unpack it in a directory you also use for real work.
Fetching is one thing and installing is another. Installing can trigger scripts and builds. The steps below only fetch and unpack.
npm: fetch two tarballs
The npm documentation says npm pack takes a package spec such as a name and version, fetches it to the cache, and copies a tarball named name-version.tgz into the current directory.
mkdir old new
cd old && npm pack some-package@1.2.3 && tar -xzf some-package-1.2.3.tgz && cd ..
cd new && npm pack some-package@1.2.4 && tar -xzf some-package-1.2.4.tgz && cd ..
diff -ru old/package new/package
To find the previous version, npm view some-package versions lists the history, as the npm docs show.
PyPI: download without installing
The pip documentation includes a pip download command. Prefer wheels, since a source distribution may need a build step to produce metadata, and keep dependencies out of the way.
pip download some-project==1.2.3 --no-deps --only-binary=:all: -d old
pip download some-project==1.2.4 --no-deps --only-binary=:all: -d new
A wheel is a zip archive, so you can unzip it and compare the folders the same way. If only a source distribution exists, unpack it and read it, but do not build it on a machine you care about.
What to look for in the diff
| Change | Why it matters |
|---|---|
| New or changed install scripts | They run on the installing machine |
| New network calls or hard coded hosts | Possible data exfiltration or a downloader |
| Obfuscated or minified blobs in a package that was readable before | Hides intent |
| New files with no matching source repository commit | Suggests the release was not built from the public code |
| Changed dependency list | A new dependency can carry the payload |
A large diff is not proof of anything. A normal release changes many files. Focus on the kind of change, not the count.
Compare against the repository
Check whether the new release matches a tagged commit in the public source. A release with files that exist nowhere in the repository deserves a closer look. Our post on whether a published package can differ from its source repository covers why that happens and how to read it.
Write down what you found
If the diff looks clean, record that you compared the two versions and what you checked, but do not call the package safe. A diff shows differences, not intent. If it looks wrong, stop installing the version, tell your team, and follow your incident runbook.
A worked example
Imagine a story says version 2.6.0 of a small utility package was altered. Your service uses it. You set up a disposable container with no secrets and run the npm steps from above.
The diff output shows three things.
- The
package.jsonfile has a newscriptsentry that runs a file namedsetup.jsafter install. setup.jsis new, about 4 KB, and mostly one long line of encoded text.- The list of dependencies gained one package you have never heard of.
None of these proves harm on its own. Put together, they form the pattern you were warned about: a new install hook, an unreadable blob, and a new dependency. That is enough to stop using 2.6.0, tell your team, and pin the previous version while you wait for the maintainer or registry to say more.
Now a calmer outcome. The diff shows a changed version number, an updated changelog, three edited source files with readable changes, and a new test. You compare against the tagged commit in the public repository and the source files match. You record "compared 2.5.9 and 2.6.0, no install scripts, no new dependencies, source matches tag abc123" in the ticket. You have not proven the package is safe forever, but you have a documented, specific basis for carrying on.
Reading a diff without being an expert
You do not need to understand every line. Three questions get you most of the way.
- Does it run something at install time? Look at the scripts section of the manifest. For a Python wheel, look for anything unusual in the metadata and for files that run on import.
- Does it talk to the network? Search the new or changed files for hosts, URLs and words like fetch, request, socket, or exec. Add any you find to your notes.
- Is it readable? A package that was plain before and is a block of encoded text now has changed in a way worth asking about.
If you are unsure, ask someone with more experience and show them the diff. A second pair of eyes is cheap compared with a wrong call.
Tools that save time
| Need | Approach |
|---|---|
| Compare two folders | diff -ru old new, or a visual diff tool |
| See only file names that changed | diff -rq old new |
| Compare to repository source | Clone the repository at the tagged commit, then diff the folders |
| Search for hosts or URLs | grep -rE "https?://" new |
| Compare file hashes | shasum -a 256 on both trees and compare |
Run these inside the disposable environment. Copy out only your notes, not the package files.
What not to conclude
A clean diff does not make a release safe, because a malicious change might be subtle or live in a dependency. A noisy diff does not make a release malicious, because big releases are normal. Report what you saw and let the owner or the wider community weigh in. If you believe you have found something, report it to the registry's security contact and the maintainer through their documented channels.
Document what you did
Record the two versions, the commands, the date and time in UTC, who ran them, and a one paragraph summary. If you decide to block the version, add the reason. This is the evidence trail that lets you defend the decision later.
Try it yourself
Click through it at your own pace, or open it full screen. Look at how every component and version gets an entry, which tells you which release you would be comparing.
Quick check
Does npm pack run the package's code?
The npm docs describe it as fetching a tarball and copying it to your directory. Still, handle the contents in a disposable environment and never run them.
Why use the no-deps flag with pip download?
It keeps the download to the one package you want to inspect instead of fetching its whole dependency tree.
Do I need to be a malware analyst to do this?
No. Checking for install scripts, new dependencies, network calls and unreadable blobs catches many obvious cases. For anything unclear, ask a colleague with more experience.
Sources
Last reviewed: 19 October 2026.