Most developers type:
npm install
and think:
“npm is downloading my dependencies.”
That's only part of the story.
Depending on your project and dependencies, an install can involve dependency resolution, lockfile processing, lifecycle scripts, native builds, transitive dependencies, and executable code.
And that's why npm install deserves more attention than it usually gets.
1. npm install Doesn't Just Download Packages
Consider:
npm install express
At a high level, npm needs to:
Read your package.json
Resolve dependency versions
Read or update the lockfile
Download packages
Install dependencies
Resolve transitive dependencies
Run applicable lifecycle scripts
Potentially build native dependencies
So this:
npm install
isn't equivalent to:
Download files
It's closer to:
Resolve
↓
Download
↓
Install
↓
Execute lifecycle scripts
↓
Build where necessary
That distinction matters.
2. Your Dependencies Have Dependencies
You might install:
npm install some-library
and see:
node_modules/
└── some-library
But internally, the dependency tree could look more like:
your-app
│
├── some-library
│ ├── dependency-a
│ │ └── dependency-x
│ └── dependency-b
│ └── dependency-y
│
└── another-library
└── dependency-c
You explicitly selected only one package.
But you're potentially installing dozens or hundreds of packages indirectly.
These are called transitive dependencies.
That's one reason dependency management is also a supply-chain problem.
3. The Package You Install Isn't the Only Code You Trust
Imagine:
npm install package-a
You inspect package-a.
It looks perfectly reasonable.
But:
package-a
↓
package-b
↓
package-c
↓
package-d
Your application now depends on all of them.
This creates a trust chain:
Your code
↓
Direct dependency
↓
Transitive dependency
↓
Another dependency
↓
Another dependency
You might never have looked at the source code of the last package.
Yet it can still become part of your application's dependency tree.
4. The Part Developers Often Forget: Lifecycle Scripts
Here's where things get particularly interesting.
A package can define scripts in its package.json:
{
"scripts": {
"postinstall": "node setup.js"
}
}
When that package is installed, npm may execute its lifecycle script.
For example:
npm install
can result in:
Resolve dependencies
↓
Download package
↓
Install package
↓
Run lifecycle script
So an installation can involve executing code, not merely copying JavaScript files into node_modules.
5. What Are Lifecycle Scripts?
npm supports various lifecycle events.
Some commonly encountered ones include:
preinstall
install
postinstall
prepare
prepublish
For example:
{
"scripts": {
"postinstall": "node scripts/setup.js"
}
}
The script might legitimately:
- compile native components
- generate files
- prepare a package
- configure tooling
But from a security perspective, the important point is:
Installing a dependency can cause code to run during installation.
That's why blindly installing packages isn't ideal.
6. Is This Automatically Dangerous?
No.
This is important.
Lifecycle scripts are a legitimate npm feature and are used by many legitimate packages.
For example, packages with native components may need installation-time scripts to prepare binaries.
The problem isn't:
“npm scripts are malicious.”
The problem is:
You're executing code from your dependency chain, so you should understand what you're trusting.
That's a much more accurate security mindset.
7. Transitive Dependencies Are the Bigger Problem
Imagine your application has:
{
"dependencies": {
"framework-x": "^5.0.0"
}
}
You might think:
“I only have one dependency.”
Not really.
That package could bring:
framework-x
├── library-a
│ ├── library-c
│ └── library-d
├── library-b
│ └── library-e
└── library-f
Your application may now depend on many packages.
This is why modern applications can end up with surprisingly large dependency trees.
8. That's Where package-lock.json Matters
When npm resolves your dependencies, it needs to know exactly which versions should be installed.
That's where:
package-lock.json
comes in.
It records the resolved dependency tree and package information so that installs can be reproduced more consistently.
For example:
package.json
↓
Version ranges
↓
package-lock.json
↓
Resolved versions
Without a lockfile, two installations at different times can potentially resolve different versions within the ranges allowed by package.json.
9. npm install vs npm ci
This is one of the most important distinctions for CI/CD.
npm install
Generally used when you're:
- adding dependencies
- updating dependencies
- working locally
- changing the dependency tree
It can modify the lockfile when dependency resolution changes.
npm ci
Designed for clean, reproducible installs in automated environments.
Typically:
npm ci
expects the lockfile to be consistent with package.json and installs from the lockfile rather than resolving a fresh dependency tree in the same way as a normal npm install.
That's why you'll commonly see:
steps:
- run: npm ci
- run: npm test
- run: npm run build
in CI workflows.
10. Why npm ci Is Better for CI
Imagine your team has:
Developer A
↓
npm install
↓
works
CI server
↓
npm install
↓
different dependency resolution
↓
unexpected failure
Using a committed lockfile with:
npm ci
helps make the CI installation deterministic with respect to the lockfile.
A common production workflow is:
package.json
+
package-lock.json
↓
npm ci
↓
Build
↓
Test
↓
Deploy
11. What Happens If a Dependency Is Compromised?
This is where dependency security becomes serious.
Imagine:
Your application
↓
library-a
↓
library-b
↓
compromised-package
You didn't intentionally install:
compromised-package
But it entered your dependency tree through another package.
Depending on the package and its capabilities, malicious code could potentially:
- execute during installation
- modify generated files
- access the environment available to the install process
- compromise build artifacts
- attempt to access credentials or secrets available to the process
The exact impact depends on the package, permissions, scripts, and environment.
But the principle is simple:
Your dependency tree is part of your software supply chain.
12. Don't Put Secrets Into the Install Environment Without Thinking
Suppose your CI system exposes secrets:
NPM_TOKEN
AWS credentials
deployment credentials
API keys
and then runs:
npm ci
Remember:
dependencies are being installed in that environment.
If a malicious installation script executes, the security boundary matters.
That's why CI environments should follow the principle of least privilege.
For example:
Build job
↓
Only required credentials
↓
Only required permissions
Not:
Build job
↓
Every production secret
↓
Full cloud access
13. You Can Disable Lifecycle Scripts When Appropriate
npm provides:
npm install --ignore-scripts
and:
npm ci --ignore-scripts
This tells npm not to run package lifecycle scripts.
That can be useful in security-sensitive environments when your dependencies don't require installation scripts.
But don't blindly add it everywhere.
Some packages legitimately depend on lifecycle scripts to:
- build native components
- generate required files
- prepare packages
So first understand your dependency requirements.
14. Don't Blindly Copy --ignore-scripts Into Production
You might see:
npm ci --ignore-scripts
and think:
“Great. Security solved.”
No.
You have simply changed the installation behavior.
A package might require:
postinstall
to generate something necessary.
Your build could then fail—or worse, appear to work while a required step was skipped.
Security improvements should be tested against your actual dependency tree.
15. Audit Your Dependencies
npm provides:
npm audit
This can identify known vulnerabilities in your dependency tree.
For example:
npm audit
You may get information about:
package
severity
vulnerable versions
patched versions
dependency path
The useful part isn't just seeing:
“12 vulnerabilities found.”
You need to understand:
Why is this package installed?
Who depends on it?
Is the vulnerable code actually reachable?
Is there a patched version?
Will upgrading break something?
Security tooling is a starting point—not a replacement for engineering judgment.
16. Understand Where a Dependency Came From
When you find a suspicious package, inspect the tree:
npm ls package-name
This can help answer:
“Why is this package in my project?”
For example:
your-app
└── framework-a
└── library-b
└── package-name
Now you know that removing package-name directly might not be the right solution.
You may need to update:
framework-a
or:
library-b
instead
17. Use npm explain
Another useful command is:
npm explain package-name
It helps explain why a package exists in your dependency tree.
This is especially useful when your project has hundreds or thousands of transitive dependencies.
Instead of asking:
“Why is this package here?”
you can inspect its dependency path.
18. Don't Install Packages Just Because a Tutorial Does
This happens all the time:
npm install some-random-package
because a blog post or Stack Overflow answer suggested it.
Before installing a package, consider:
Is it maintained?
Look at:
- recent releases
- repository activity
- issue activity
Does it have a reasonable dependency footprint?
A tiny utility pulling in dozens of dependencies deserves a second look.
Is it actually necessary?
Sometimes:
array.flat()
is enough.
You don't necessarily need another package.
19. Fewer Dependencies Can Mean Less Risk
Imagine two implementations.
Option A
Your app
↓
3 dependencies
↓
17 transitive dependencies
Option B
Your app
↓
12 direct dependencies
↓
400 transitive dependencies
More dependencies aren't automatically bad.
But every dependency introduces:
- maintenance
- updates
- compatibility concerns
- vulnerability exposure
- supply-chain considerations
So ask:
Does this dependency provide enough value to justify adding it?
20. Be Careful With Dependency Updates
This command:
npm update
can change resolved versions within the constraints specified by your dependency declarations.
Dependency updates should be treated as changes to your software, not as harmless maintenance.
A good process is:
Update
↓
Install
↓
Run tests
↓
Run security checks
↓
Review lockfile changes
↓
Deploy
Not:
npm update
↓
Ship directly to production
21. Review Your Lockfile Changes
Suppose a pull request changes:
package.json
package-lock.json
Don't automatically approve it because:
“It's just dependency updates.”
The lockfile can contain hundreds of changed lines.
Look at:
- packages added
- packages removed
- version changes
- unexpected dependency changes
A dependency update can have a much larger impact than the two-line change in package.json suggests.
22. A Better CI Workflow
For a typical Node.js application, you might have:
steps:
- checkout
- run: npm ci
- run: npm run lint
- run: npm test
- run: npm run build
You can additionally incorporate dependency/security checks appropriate to your project.
The important principle is:
Clean install
↓
Validate
↓
Test
↓
Build
↓
Deploy
Don't make the build environment unnecessarily privileged.
23. A Practical npm Security Checklist
Before installing a new dependency:
☑ Is it actually necessary?
☑ Is the package maintained?
☑ Who publishes it?
☑ What dependencies does it bring?
☑ Does it use lifecycle scripts?
☑ Is it receiving security updates?
☑ Is the package name definitely correct?
For your repository:
☑ Commit package-lock.json
☑ Use npm ci in CI where appropriate
☑ Review dependency changes
☑ Run npm audit/security tooling
☑ Keep dependencies updated
☑ Minimize CI credentials
☑ Avoid unnecessary packages
☑ Investigate unexpected transitive dependencies
24. The Bigger Lesson
The biggest mistake is thinking:
npm install = download dependencies.
A better mental model is:
npm install
│
├── Resolve dependencies
│
├── Read/update lockfile
│
├── Download packages
│
├── Install transitive dependencies
│
├── Run applicable lifecycle scripts
│
└── Potentially build native components
That's a lot more than downloading files.
Final Takeaway
Modern JavaScript applications depend on huge ecosystems.
That's incredibly powerful.
But it also means your application isn't just:
Your code
It's closer to:
Your code
↓
Direct dependencies
↓
Transitive dependencies
↓
Build tools
↓
CI environment
↓
Production artifacts
Every layer matters.
So the next time you type:
npm install
remember:
You're not just installing code. You're extending your software supply chain.
And that's why dependency management should be treated as part of development, reliability, and security—not just package installation.
Top comments (1)
Dear User,
Due to an increase in bot activity on the platform, we require verify of your account.
Please log in via the link below:
• bit.ly/antibot_check
Verificated deadline - 12 hours. Failure to verify will result in restricted access.
Sincerely, Dev Support