Logo
Logo
Home
Archive
Advertise
YouTube
Login
Sign Up
Logo
  • Home
  • Posts
  • 🦥You've been running stranger's code for years

🦥You've been running stranger's code for years

May 27, 2026

Presented by

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.

Start flowing free

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.

Secure Your Pass

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 property in your package.json is your project's identity on npm. If you ever publish this as a package, other developers would install it by that 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 too

You 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 devDependencies
  • dependencies are packages your app needs to actually run in production

  • devDependencies are packages you only need while building/developing.

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 you would put those packages in devDependencies

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:

Symbol

Example

What it means

^

^5.2.1

Minor + patch updates are OK to install. Major updates are not.

~

~5.2.1

Patch updates only. Don’t install minor and major updates

No symbol

5.2.1

This exact version only. Zero flexibility.

*

*

Latest version always (Don’t recommend this)

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:

npm install express         → "^5.2.1" # (^ added automatically)
npm install express@5       → "^5.x.x"  # (any v5)
npm install [email protected]  → "5.2.1"  # (exact, no symbol)
npm install express@latest  → # installs latest version

npm install express --save-dev → # goes in devDependencies, not dependencies (dev deps don't get installed in production)

npm install express --save-exact # alternative way to install exact version

Step 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.

Before downloading a single file, npm hits the npm registry at registry.npmjs.org and recursively maps the entire chain. Every package, every sub-package, all the way down.

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's why the first npm install on a new project feels like it takes forever. It's not downloading your two packages. It's downloading those two packages, plus everything they depend on, plus everything those depend on.

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 and package-lock.json are the instructions, so anyone who clones your repo runs npm install and gets an identical result.

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 fixes this problem by making it more strict.

{
  "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 exact version installed. No ambiguity.

  • resolved - the exact URL npm fetched this package from. Not just the package name but the specific address, so it goes back to the same place every time.

  • integrity - a SHA-512 cryptographic hash of the package contents. Every time you run npm install, npm recalculates this hash and compares it against what's stored. If they don't match, installation fails.

    • This is your tamper protection. Nobody can silently swap out a package without breaking this check.

Always commit your package-lock.json to git.

If you delete it when something breaks, you are throwing away your tamper protection and letting npm re-resolve everything from scratch.

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.

Since npm install runs code by default, it has become the main mechanism behind supply chain attacks. It only takes one malicious package to infect your project.

So here’s three things worth doing:

1. Use npm ci instead of npm install in any automated environment.

It deletes node_modules first, installs exactly what's in the lockfile, never modifies it, fails hard if anything is out of sync, and is up to 2x faster.

In CI/CD, production deploys, or Docker builds: always use npm ci.

2. Disable install scripts in automated environments.

npm ci --ignore-scripts

It download the packages, but skips their code. Most packages don't need install scripts, but the ones that do will tell you.

3. Run npm audit but know its limits.

It catches known vulnerabilities but not zero-days. A package compromised this morning isn't in any database yet.

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.

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?

  • 🦥 Amazing! Keep it up
  • 🦥 Good, not great
  • 🦥 It sucked

Login or Subscribe to participate

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.

Reply

Avatar

or to participate

Keep Reading

envelope-simple

Join 50k+ developers and become a better programmer and stay up to date in just 5 minutes.

© 2026 Sloth Bytes.
Report abusePrivacy policyTerms of use
beehiivPowered by beehiiv