Hello cuties
Welcome to another edition of Sloth Bytes. I hope you’re having a good week.

Your prompts are leaving out 80% of what you're thinking.
When you type a prompt, you summarize. When you speak one, you explain. Wispr Flow captures your full reasoning — constraints, edge cases, examples, tone — and turns it into clean, structured text you paste into ChatGPT, Claude, or any AI tool. The difference shows up immediately. More context in, fewer follow-ups out.
89% of messages sent with zero edits. Used by teams at OpenAI, Vercel, and Clay. Try Wispr Flow free — works on Mac, Windows, and iPhone.
The World's Biggest Dev Event Hits Silicon Valley
From AI and cloud to DevOps and security — WeAreDevelopers World Congress brings the entire modern stack to San Jose. 500+ speakers. 10,000+ developers. One epic September. Use code GITPUSH26 for 10% off.

How npm install Actually Works

Over the last 2 weeks I’ve talked about TeamPCP and how they’ve managed to infect a lot of packages which led to them breaching GitHub through a poisoned VS Code extension.
So it had me thinking and some of you probably thought the same:
"How did installing a package even cause all that?"
Most of us just type npm install and trust that something good comes out the other side.
So let me show you exactly what's happening when you run that command. And more importantly, what you should actually be doing to protect yourself.
Step 1: npm reads package.json
When you run npm install, the first thing npm does is open your package.json and look at your dependencies.
{
"name": "goofy-project",
"version": "1.0.0",
"scripts": {
"start": "node index.js",
"dev": "tsx watch index.ts",
"build": "tsc",
"test": "jest"
},
"dependencies": {
"express": "^5.2.1",
"dotenv": "^16.4.7",
"zod": "^4.4.3"
},
"devDependencies": {
"typescript": "^5.0.0",
"jest": "^29.5.0",
}
}
You've probably seen this file a thousand times without thinking about what they actually mean.
name and version
The name field identifies the package/project. If you publish it to an npm registry, that published name is what other developers install (subject to scopes/name availability). For private apps that are never published, it’s still useful project metadata, but it does not magically reserve a public npm package name.
npm install goofy-project
For private projects that never get published, it’s just metadata, but can still be useful for documentation.
scripts
Scripts is where npm run dev, npm test, and all your custom commands come from.
npm just runs whatever shell command you put in the value. That's it.
npm run dev # runs "tsx watch index.ts"
npm run build # runs "tsc"
npm test # "test" is special, no "run" needed
npm start # "start" is special tooYou can put anything in scripts. Build commands, database migrations, deployment scripts. Most teams use this as the single source of truth for how to do things in a project.
dependencies vs devDependencies
This is one of the most misunderstood parts of package.json.
npm install express # goes into dependencies
npm install jest --save-dev # goes into devDependenciesdependencies are packages your application/library needs as runtime dependencies of the thing you ship.
devDependencies are development/build/test tooling that consumers or a production runtime may not need.
Examples:
TypeScript compiles your code but the compiled output doesn't need TypeScript to run.
Jest runs your tests in development but nobody's running tests in production.
So tools like Jest and often TypeScript belong in devDependencies. One nuance: npm installs devDependencies by default for a normal local install. They are omitted only when your install/config explicitly omits the dev dependency type (for example, a production-focused install configuration).
What do those version numbers mean?
npm uses semantic versioning (semver).

Every version number has three parts:
Major: breaking changes. Things probably work differently now.
Minor: new features, nothing existing is broken.
Patch: bug fixes only.
Some versions also contain symbols, which tell npm how flexible it can be when picking a version:
Specifier | Example | Rough meaning |
|---|---|---|
|
| Allow updates that do not change the left-most non-zero component: |
|
| Approximately patch-level updates: |
Exact |
| Only that version satisfies the specifier. |
|
| Any version satisfies the range. Usually far broader than you want for an application dependency. |
Most packages default to ^ when you run npm install. Which is usually fine.
Personally I would avoid using *. Latest version sounds nice on paper, but sometimes these packages can have breaking API changes and if you have a large project, that usually includes a lot of refactoring. I don’t think you want that as a surprise.
Quick cheat sheet for installing packages:
# Install the latest version matching the package's default tag.
# npm normally saves it using your configured save-prefix (commonly ^).
npm install express
# Install a version matching the v5 range; npm resolves a concrete version.
npm install express@5
# Request 5.2.1, but note: your save-prefix config may still save a range.
npm install [email protected]
# Save the resolved version exactly with no range prefix.
npm install [email protected] --save-exact
# Save to devDependencies.
npm install jest --save-devStep 2: npm resolves the full dependency tree
Here's where it gets interesting.
You installed something like express, but express depends on other packages. And those packages depend on other packages. And those packages...
You get it.

Actual visual of what packages express depends on.
npm reads dependency metadata from the configured registry/cache and builds a dependency tree that satisfies package ranges, peer-dependency constraints, platform rules, lockfile information, and other configuration. The real implementation is more interleaved than a perfectly neat “resolve everything, then download everything” flow, but dependency resolution is the key idea.
This is called dependency resolution. npm has to figure out which version of every package satisfies all the declared constraints simultaneously, handle conflicts where two packages need different versions of the same dependency, and do this across potentially hundreds of packages at once.
That transitive dependency graph is why installing one top-level package can bring in many more packages. Network latency, cache state, lifecycle scripts, native builds, and the size/shape of the tree can all affect install time.
Step 3: Downloads into node_modules
Once npm has the full resolved list, it downloads everything as compressed files from the registry and extracts them into your node_modules folder.

Large projects can have gigabytes of node_modules. Which is one of the reasons why node_modules is in your .gitignore. There’s other reasons too:
It's machine-specific. Some packages compile native binaries. A node_modules built on Mac M3 isn't guaranteed to work on a Linux server.
It changes constantly. Every install regenerates thousands of files. Your git diff would be unreadable.
Once again, it's enormous. GitHub even has a 100MB file size limit and will probably reject your push if you try committing it.
The whole system runs on one contract: commit the instructions, not the output.
package.json declares your dependency intent and package-lock.json records a concrete dependency tree. With a committed lockfile and compatible npm/config/platform conditions, installs can be highly reproducible—but native/optional dependencies, npm versions, OS/CPU differences, and install flags can still affect the resulting environment.
Step 4: package-lock.json
Most developers know the lockfile exists, but I’m positive they don’t know why it exists.
package.json can store version ranges, which is great, but two developers cloning the same repo on different days could end up with 5.2.1 and 5.3.0. Which sounds harmless until one of them introduces a bug that only shows up on one machine.
package-lock.json addresses this by recording a concrete dependency tree, including exact resolved versions and integrity information, so npm does not have to freely choose a new compatible tree on every install.
{
"name": "goofy-project",
"lockfileVersion": 3,
"packages": {
"node_modules/express": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz",
"integrity": "sha512-ab1234..."
}
}
}Three fields doing all the work:
version — the concrete package version represented at that location in the dependency tree.
resolved — when present, where npm resolved/fetched that artifact from (the exact representation varies by dependency/source and lockfile version).
integrity — a Subresource Integrity string (commonly SHA-512) that lets npm verify the artifact it unpacked matches the bytes recorded by the lockfile.
This protects against the fetched artifact silently differing from the locked artifact. It does not prove the package author/account was trustworthy when that version was originally published, and it does not detect malicious code that is already part of the locked package.
For applications, commit your package-lock.json. It is part of the reproducible build/supply-chain record and should usually change in code review alongside dependency changes.
Deleting a lockfile because “dependencies are weird” discards the exact tree and integrity records you were using, then asks npm to resolve a new tree from the version ranges. Sometimes regenerating a lockfile is intentional—but treat it like a dependency update, review the diff, and test it rather than using deletion as a magic repair button.
npm is very fragile - The left pad incident
In 2016, a developer named Azer Koçulu got into a dispute with a company over a package name. npm sided with the company. So he deleted all 273 of his packages from the registry in protest.
One of them was called left-pad. An 11-line function that adds spaces to the left of a string:
module.exports = leftpad;
function leftpad (str, len, ch) {
str = String(str);
var i = -1;
ch || (ch = ' ');
len = len - str.length;
while (++i < len) {
str = ch + str;
}
return str;
}The moment left-pad was removed, React broke, Babel broke, projects at Facebook, Netflix, and Spotify failed to build because of 11 lines of code written by one person.
npm changed their unpublish policy after this. You can't delete a widely-used package once 24 hours have passed and other projects depend on it.
But everyone learned a lesson here: Every package in your node_modules is written and maintained by a human who could change their mind, get their account compromised, or just disappear.
Ways to protect yourself from malicious packages.
npm can run dependency lifecycle scripts during installation, which makes package installation a meaningful supply-chain execution boundary. Malicious packages can also hurt you when imported/executed later, through compromised build tooling, stolen maintainer accounts, typosquatting, dependency confusion, or vulnerable transitive dependencies—so “install scripts” are only one part of the threat model.
So here’s three things worth doing:
1. Use npm ci instead of npm install in any automated environment.
npm ci requires an existing lockfile, removes the existing node_modules tree, fails if package.json and the lockfile disagree, installs the locked tree, and does not rewrite package.json or the lockfile. That makes it a good default for CI and other reproducible automated builds.
Use npm ci when you want a clean, frozen install from the committed lockfile. If your lockfile was created with tree-shaping flags such as --legacy-peer-deps or other relevant config, CI needs compatible settings too.
2. Disable install scripts in automated environments.
npm ci --ignore-scripts--ignore-scripts prevents dependency lifecycle scripts from running during the install, which can reduce one execution surface. But some legitimate packages rely on install/build scripts, so this is not a free universal security switch. Use it where your dependency set supports it, or explicitly allow/review the scripts your build actually requires.
3. Run npm audit but know its limits.
npm audit can report vulnerabilities known to the advisory sources it uses, but it cannot tell you that every package is trustworthy, catch every malicious release, or guarantee safety against a new compromise. Treat it as one signal alongside lockfile review, dependency-update tooling, provenance/signature controls where available, least-privileged CI credentials, and minimizing unnecessary dependencies.
If you want a tool to scan for these bad packages you can use Socket.dev. They scan packages and prevent you from installing them.
If you want to keep learning
CI/CD explained — see where
npm ci, automated builds, tests, and deployments fit in a real pipeline.Environment variables and secrets — learn how tokens and API keys leak through Git, logs, builds, and compromised tooling.
How to contribute to open source — understand the human side of the packages and repositories your projects depend on.

I made a program that let’s me speak any language just to destroy Duolingo.
Yes. I did this instead of learning a new language. I’m too American for all that.
That’s all from me!
Have a great week, be safe, make good choices, and have fun coding.
If I made a mistake or you have any questions, feel free to comment below or reply to the email!
See you all next week.
What'd you think of today's email?
Want to advertise in Sloth Bytes?
If your company is interested in reaching an audience of developers and programming enthusiasts, you may want to advertise with us here.




