Every Node.js project accumulates vulnerable dependencies. The npm registry holds over 2.5 million packages, and researchers found 454,000 malicious or vulnerable packages uploaded in 2025 alone. Without a structured audit workflow, you are one npm install away from shipping a critical CVE to production.
npm audit is the built-in security scanner that ships with every npm version 6 and later. It cross-references your installed dependency tree against the GitHub Advisory Database, returns a severity-ranked report in seconds, and lets you patch most issues with a single command. This tutorial walks you through all 12 steps from a bare project to a fully automated vulnerability remediation pipeline. Estimated time: 30 minutes.
Prerequisites: Versions, Tools, and Project Setup
Before running your first audit, confirm you have the following in place. Mismatched versions are the single most common source of confusing audit output.
| Requirement | Minimum Version | Why It Matters |
|---|---|---|
| Node.js | 18 LTS (18.20+) | npm 9 ships with Node 18; older npm lacks JSON audit schema v2 |
| npm | 9.x (bundled with Node 18) | npm 7+ rewrote audit to support workspaces and lockfile v2 |
| package-lock.json | lockfileVersion 2 or 3 | Audit reads the lockfile, not just package.json; missing lockfile = no results |
| Git | Any recent version | Track lockfile changes after every npm audit fix run |
| A CI system | GitHub Actions, GitLab CI, or Jenkins | Automate audit on every push in Step 9 |
Verify your environment before continuing:
node --version # Should print v18.x.x or higher
npm --version # Should print 9.x.x or higher
ls package-lock.json # Must exist; if missing, run npm install first
If you are running npm 6 (shipped with Node.js 10 and 12), upgrade before proceeding. npm 6 uses a deprecated audit endpoint that returns schema v1 output, and many remediation flags behave differently. Run npm install -g npm@latest to upgrade to the current stable release.
You also need a project with at least one dependency installed. If you are starting from scratch, create a minimal Express project to follow along:
mkdir audit-demo && cd audit-demo
npm init -y
npm install express lodash axios semver
This installs four packages that have historically had known vulnerabilities across various version ranges, making them useful for demonstrating audit output in a controlled way. In a real project, your node_modules will contain dozens or hundreds of transitive dependencies with varying vulnerability histories.
Step 1: Run Your First npm audit
Navigate to your project root and run the base command with no flags:
cd audit-demo
npm audit
npm sends a compressed description of your dependency tree to the npm registry. The registry matches each package version against its advisory database and returns a structured report. The scan runs client-side against the lockfile, so no actual package code leaves your machine. Only version metadata is transmitted to the registry endpoint.
A typical output for a project with known vulnerabilities looks like this:
# npm audit report
lodash <4.17.21
Prototype Pollution in lodash - https://github.com/advisories/GHSA-jf85-cpcp-j695
fix available via `npm audit fix`
node_modules/lodash
semver <7.5.2
Regular Expression Denial of Service in semver
fix available via `npm audit fix`
node_modules/semver
node_modules/node-gyp/node_modules/semver
axios <1.6.0
Server-Side Request Forgery in axios - https://github.com/advisories/GHSA-wf5p-g6vw-rhxx
fix available via `npm audit fix`
node_modules/axios
4 vulnerabilities (1 moderate, 2 high, 1 critical)
To address all issues, run:
npm audit fix
At the bottom, npm prints a summary: the total vulnerability count broken down by severity. If you see “0 vulnerabilities,” your current lockfile is clean against known advisories. That does not mean your dependencies are perfectly secure, but it confirms no registered CVEs affect your installed versions at that moment.
Step 2: Read the Audit Report Correctly
Each vulnerability block in the human-readable report contains five pieces of information you need to act on before making any changes to your dependency tree.
| Field | What It Tells You | How to Use It |
|---|---|---|
| Package name + version range | Which versions are vulnerable | Compare against your installed version in package-lock.json |
| Advisory title | The vulnerability class (injection, DoS, prototype pollution) | Assess exploitability for your specific usage pattern |
| Advisory URL (github.com/advisories/GHSA-…) | Full CVE mapping, CVSS score, proof-of-concept | Read this before deciding to suppress or defer |
| “fix available via” line | Whether a safe fix exists vs. requires –force | Determines your remediation path |
| node_modules path | Direct dependency vs. transitive (nested path) | Transitive vulnerabilities require updating the parent package |
The path field is the most underread piece of the report. When you see a two-level path like node_modules/node-gyp/node_modules/semver, the vulnerable package is a transitive dependency of node-gyp. You cannot fix it by updating semver directly in your package.json. You must either update node-gyp to a version that pulls in a fixed semver, use npm audit fix to let npm resolve the tree automatically, or apply an npm overrides entry to force a specific sub-dependency version.
For transitive vulnerabilities with no upstream fix, always click through to the advisory page linked in the report. CVSS scores between 0 and 3.9 are low severity. Scores from 4.0 to 6.9 are moderate. Scores from 7.0 to 8.9 are high. Scores from 9.0 to 10.0 are critical. A critical CVSS score on a package you only use in a test helper is a very different risk profile than the same score on a package sitting in your authentication middleware.
Step 3: Understand npm Severity Levels and Response SLAs
npm audit uses four severity levels, each with a distinct remediation priority. Treat these as minimum SLAs, not aspirational targets:
| Severity | CVSS Range | Example Vulnerability Class | Recommended SLA |
|---|---|---|---|
| Critical | 9.0 – 10.0 | Remote code execution, authentication bypass | Patch within 24 hours |
| High | 7.0 – 8.9 | Privilege escalation, SSRF, path traversal | Patch within 7 days |
| Moderate | 4.0 – 6.9 | RegEx DoS, information disclosure, CSRF | Patch in next sprint |
| Low | 0.1 – 3.9 | Timing side-channel, verbose error messages | Patch in next scheduled update |
In 2026, the most actively exploited npm vulnerability classes are prototype pollution (affects lodash, merge-deep, and similar object utilities), ReDoS or Regular Expression Denial of Service (affects packages that accept user-controlled regex patterns), path traversal in file-serving packages, and command injection in packages that shell out to the operating system. Check every critical and high finding against these classes before deferring.
High-severity findings in development dependencies carry lower real-world risk than the same finding in a production dependency, because dev packages never execute in the production runtime. This is why Step 7 covers npm audit --omit=dev separately. Do not let dev-only vulnerabilities block your production deployment pipeline unless your CI artifacts ship dev dependencies to a live server.
One nuance that most tutorials omit: severity in the npm advisory database reflects the potential impact of a vulnerability in a worst-case deployment scenario, not necessarily in your specific application. A moderate SSRF finding in a package you use only for string formatting is not exploitable as SSRF in your codebase. Assess each finding in context, but document your assessment. Blind suppression and informed risk acceptance are not the same thing.
Step 4: Fix Vulnerabilities Automatically with npm audit fix
npm audit fix is the first remediation pass you should always try. It installs compatible updates for vulnerable packages without bumping any major version number:
npm audit fix
npm resolves the minimum version change that eliminates each vulnerability while respecting the semver ranges declared in your package.json. The command updates package-lock.json and installs the new versions into node_modules. It does not modify package.json itself unless a direct dependency needed a patch that could not be installed within the existing declared range.
After running npm audit fix, run npm audit again immediately to check the remaining count. Some vulnerabilities require multiple fix passes because patching one package exposes a previously hidden transitive dependency:
npm audit fix
npm audit
# Expected output after full resolution:
found 0 vulnerabilities
# If issues remain, the output will say:
# 2 vulnerabilities (1 moderate, 1 high)
# To address issues that do not require attention, run:
# npm audit fix
# To address all issues (including breaking changes), run:
# npm audit fix --force
Always commit the updated package-lock.json immediately after a successful fix run. Failing to commit it means your next npm ci call in CI will restore the vulnerable versions from the old lockfile. Treat the lockfile as a first-class security artifact, not a generated file you can leave uncommitted.
Run your test suite after every npm audit fix call, even when no --force flag was used. Patch-level updates occasionally introduce unexpected behavior changes that pass the semver contract but break application assumptions. Your tests are the fastest signal that something changed:
npm audit fix && npm test
Step 5: Handle Breaking Changes with –force
npm audit fix --force allows npm to install major-version bumps. This is a significant behavior change from the default fix command. Major version bumps can break your application because they may remove APIs your code depends on, change default behavior, alter request or response formats, or drop support for older Node.js versions.
# Never run this blindly on a production or main branch
npm audit fix --force
# Run your full test suite immediately after
npm test
# If tests pass, review exactly what changed
git diff package-lock.json | head -80
git diff package.json
A safer workflow for --force scenarios follows a four-step process. First, create a dedicated branch for the dependency upgrade: git checkout -b deps/security-fix-june-2026. Second, run npm audit fix --force on that branch only. Third, run your full test suite and manually smoke-test every feature that uses the upgraded package. If tests fail, inspect the changelog for each bumped major version to identify which breaking changes affect your code. Fourth, fix your application code to be compatible with the new major version, then open a pull request with both the lockfile changes and all application code updates together so reviewers have full context and can verify the tests pass.
If you cannot safely apply a major version bump and the vulnerability is critical, you have two intermediate options. You can apply an npm overrides field as described in Step 10 to pin the sub-dependency to a fixed version. Alternatively, you can temporarily isolate the vulnerable code path behind a feature flag or remove the feature entirely until a compatible fix is available.
Step 6: Parse JSON Output for Scripts and Dashboards
The human-readable output is not machine-parseable in a stable way. For CI/CD integrations, security dashboards, Slack alerts, and automated triage scripts, use the JSON flag:
npm audit --json > audit-report.json
The JSON output follows schema v2 (npm 7+). The top-level structure contains a vulnerabilities object keyed by package name, a metadata block with severity counts, and an auditReportVersion field set to 2. Here is how to extract the critical and high count programmatically and fail a CI step when those thresholds are exceeded:
// check-audit.js - run with: node check-audit.js
const fs = require('fs');
const report = JSON.parse(fs.readFileSync('./audit-report.json', 'utf8'));
const { critical, high, moderate, low } = report.metadata.vulnerabilities;
const total = report.metadata.vulnerabilities.total;
console.log(`Audit summary: ${total} total vulnerabilities`);
console.log(` Critical: ${critical}`);
console.log(` High: ${high}`);
console.log(` Moderate: ${moderate}`);
console.log(` Low: ${low}`);
// List all critical and high findings with package names and advisory URLs
if (critical > 0 || high > 0) {
console.error('\nACTION REQUIRED - Critical/High vulnerabilities:');
for (const [pkg, vuln] of Object.entries(report.vulnerabilities)) {
if (vuln.severity === 'critical' || vuln.severity === 'high') {
const advisory = vuln.via.find(v => typeof v === 'object');
const url = advisory ? advisory.url : 'No advisory URL';
console.error(` [${vuln.severity.toUpperCase()}] ${pkg} - ${url}`);
}
}
process.exit(1);
}
console.log('\nPASS: No critical or high vulnerabilities found');
The vulnerabilities object in schema v2 maps each vulnerable package name to a descriptor. The key fields you will use in automation scripts are:
| JSON Field | Type | Meaning |
|---|---|---|
severity | string | “critical”, “high”, “moderate”, or “low” |
isDirect | boolean | true if your package.json lists this package directly |
via | array | Advisory details or parent packages that pull this dependency |
effects | array | Packages in your tree that depend on this vulnerable package |
fixAvailable | boolean or object | false = no fix; object = fix requires version bump of this parent |
range | string | Semver range of affected versions |
Use the via array to trace the full advisory chain for each finding. When via contains another package name (a string rather than an advisory object), it means the vulnerability is transitive and arriving through that parent package, not from a directly installed advisory. This distinction matters for remediation: you need to update the parent, not the vulnerable child.
Step 7: Audit Only Production Dependencies
Dev dependencies (listed under devDependencies in package.json) never ship to your production server if you use npm ci --omit=dev or NODE_ENV=production npm install. Auditing them with the same urgency as production packages creates alert fatigue and causes developers to start ignoring or disabling audit output entirely.
# Audit only packages that ship to production (npm 8+)
npm audit --omit=dev
# Equivalent flag for npm 7 and below
npm audit --production
# Combine with audit-level to set the failure threshold
npm audit --omit=dev --audit-level=high
Use --omit=dev in your CI pipeline’s production security gate step. This ensures that a vulnerable version of eslint, jest, webpack, or prettier does not block a production deployment when those tools never execute in the production runtime. Reserve the full npm audit including dev dependencies for scheduled security scans that run weekly or monthly and whose results go to your security team, not directly into merge gates.
A practical two-tier CI strategy separates the gate from the report. The gate runs npm audit --omit=dev --audit-level=high on every pull request and fails the build only when production dependencies have high or critical findings. The report runs npm audit --json as a separate scheduled job, captures everything including dev dependency findings, and uploads the result as a CI artifact for weekly security review. This keeps developers unblocked by dev-only noise while ensuring production risk is always visible and gated.
Step 8: Configure .npmrc for Team-Wide Audit Settings
The .npmrc file controls how npm behaves for your project and all team members who clone the repository. Several audit-related settings belong in this file rather than in individual npm command flags, because they apply consistently across all developer machines and CI environments:
# .npmrc — commit this file to version control
# Disable automatic audit on npm install (speeds up installs; run audit separately)
audit=false
# Set the severity level that triggers failures (options: low, moderate, high, critical)
audit-level=high
# Prevent accidental package publishing
private=true
# Use exact version pinning for new installs (stronger supply-chain protection)
save-exact=true
# Enforce lockfile usage; fail if lockfile is missing or out of sync
# (use npm ci in CI instead of npm install for the same effect)
package-lock=true
Setting audit=false in .npmrc disables the automatic audit scan that runs after every npm install. This is a common optimization for large monorepos where npm install runs dozens of times per day in CI. The audit call adds one to five seconds per install by making a network round-trip to the registry audit endpoint. Disabling it in .npmrc and running npm audit as a dedicated, isolated CI step gives you the same security coverage with none of the install overhead.
save-exact=true is a supply-chain security hardening measure that operates independently of audit. When set, npm install express writes "express": "4.19.2" to your package.json instead of "express": "^4.19.2". This prevents npm from silently upgrading to a new patch or minor release between lockfile regenerations. That matters because a compromised maintainer account can publish a malicious patch release that passes the semver caret range check and gets silently installed the next time someone regenerates the lockfile on a fresh machine.
Step 9: Integrate npm audit into GitHub Actions
A standalone npm audit step in a workflow file gates every pull request on vulnerability status automatically. Here is a production-ready GitHub Actions workflow that implements the two-tier strategy from Step 7:
# .github/workflows/security-audit.yml
name: Security Audit
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
schedule:
# Full audit every Monday at 08:00 UTC
# Catches new advisories published for existing deps between code changes
- cron: '0 8 * * 1'
jobs:
audit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Install dependencies (lockfile-strict, no scripts)
run: npm ci --ignore-scripts
- name: Production security gate (blocks merge on high/critical)
run: npm audit --omit=dev --audit-level=high
- name: Generate full audit report artifact
if: always()
run: npm audit --json > audit-report.json || true
- name: Upload audit report
if: always()
uses: actions/upload-artifact@v4
with:
name: audit-report-${{ github.run_id }}
path: audit-report.json
retention-days: 90
Three design decisions in this workflow deserve explanation. The --ignore-scripts flag on npm ci prevents postinstall scripts from executing during CI. Malicious packages often hide their payload in postinstall lifecycle hooks. Running npm ci --ignore-scripts installs the dependency tree without triggering any scripts, reducing the attack surface of the install itself. Only add specific packages to a scripts allow-list if you need their postinstall behavior.
The || true after the full audit report generation prevents the artifact upload step from being skipped when npm audit exits with code 1 (which it does whenever vulnerabilities are found at or above the configured level). The actual production gate is the dedicated step above it. The report step uses || true to ensure the artifact is always uploaded regardless of findings, so your security team can review it even when the gate passes.
The scheduled Monday run at 08:00 UTC catches new advisories published against packages you already have installed. A package that was clean when you last merged a PR can receive a new CVE registration three days later with no code change on your part. Without a scheduled run, you would not discover that finding until the next developer opens a PR and triggers the on-push audit.
Step 10: Handle Unfixable Vulnerabilities Manually
Some vulnerabilities have no available fix. The upstream package may be abandoned, the fix may require a major version bump that breaks your application, or the advisory may cover a code path your application never reaches. You have four concrete options for these cases.
Option 1: npm overrides (recommended for transitive dependencies with no upstream fix)
// package.json - add overrides at the top level
{
"dependencies": {
"some-package": "^2.1.0"
},
"overrides": {
"vulnerable-transitive-dep": "2.4.1"
}
}
// After editing package.json, regenerate the lockfile:
npm install
The overrides field, introduced in npm 8.3.0, forces every instance of the named package in your entire dependency tree to resolve to the pinned version. This is a blunt instrument: it can break packages that genuinely require the older version and depend on removed or changed APIs. Always run your full test suite after adding or modifying an override, and add a comment in package.json documenting which CVE or GHSA advisory the override addresses.
Option 2: Replace the dependency
If a package has a known vulnerability with no planned fix and an active maintained alternative exists, switch to the alternative. Check npm weekly download counts, the date of the last published version, and the GitHub repository issue tracker. An alternative with 500,000 weekly downloads and a commit within the last 30 days is almost always preferable to a package with 5,000,000 weekly downloads that has not been updated in 18 months and has an open vulnerability issue with no response from maintainers.
Option 3: Formally accept the risk
If the vulnerability exists in a dev-only package or in a code path that is architecturally unreachable in your deployment, document the decision explicitly. Many teams use a security/vulnerability-exceptions.md file with the GHSA ID, the reason for deferral, the name and date of the person who made the decision, and a next review date. Never accept a risk silently. If the advisory is later escalated by a proof-of-concept exploit, you need a clear record of why and when the deferral was made.
Option 4: Upstream contribution
For abandoned packages that are critical to your project, consider forking the package, applying the security patch, publishing the fork to npm under a scoped name such as @yourorg/package-name, and opening a pull request to the original maintainer’s repository. This is a significant time investment but is sometimes the only safe path when the package has millions of downstream dependents and no active maintainer.
Step 11: Compare npm audit, Snyk, and Dependabot
npm audit is not your only option. Three tools dominate Node.js dependency security scanning in 2026, each with different trade-offs that make them complementary rather than competing:
| Tool | Cost | Advisory Source | Fix Automation | Supply Chain Analysis | Best For |
|---|---|---|---|---|---|
| npm audit | Free (built-in) | GitHub Advisory Database | npm audit fix | None | Baseline CVE scanning, CI gating |
| Snyk | Free tier / $25/dev/mo | Snyk proprietary DB + GitHub Advisory | Automated fix PRs | License scanning, code reachability | Deeper analysis, compliance reporting |
| GitHub Dependabot | Free on GitHub | GitHub Advisory Database | Automated PRs with lockfile updates | None natively | Automated PR-based dependency updates |
| OWASP Dependency-Check | Free | NVD, OSS Index | None (report only) | None | Air-gapped environments, multi-ecosystem |
npm audit is the right default for most projects. It requires zero additional tooling, runs in under 5 seconds on a typical project with 200 dependencies, and covers the same GitHub Advisory Database that Dependabot uses. The coverage gap is in supply-chain analysis: npm audit does not detect packages that appear legitimate but were recently taken over by a new maintainer, have not yet been reported to the advisory database, or contain obfuscated malicious code with no known CVE.
According to Snyk’s npm security best practices guide, combining npm audit with a tool that analyzes code reachability reduces false-positive noise significantly. A vulnerability in a package you import but whose vulnerable function you never call is technically present but practically unexploitable. Reachability analysis can downgrade these findings so your team focuses remediation effort on what is actually reachable from your application entry points.
Dependabot is the lowest-friction option for teams already on GitHub. It opens pull requests automatically when it detects a vulnerable dependency, including transitive ones, and it updates both package.json and package-lock.json in the PR. Configure Dependabot security updates alongside your CI npm audit gate for the best combination: Dependabot handles automated patch PRs while your CI gate blocks merges that introduce new unpatched findings.
OWASP Dependency-Check fills the gap for teams in air-gapped environments or projects that mix Node.js with Java, .NET, or Python dependencies. The OWASP Dependency-Check project ships as a CLI tool and Maven, Gradle, and Jenkins plugins. It scans against the National Vulnerability Database rather than the npm-specific advisory database. It is slower than npm audit (30 to 90 seconds per scan on a typical project) but works offline once the NVD data is cached.
Step 12: Build an Automated Dependency Monitoring Workflow
A single npm audit at install time is insufficient. Advisories are published continuously throughout the week. A package that was clean in January can have a critical CVE by March with no change to your codebase. The following package.json scripts block and Dependabot configuration create a continuous monitoring workflow:
// package.json scripts block for local and CI audit workflows
{
"scripts": {
"audit:check": "npm audit --omit=dev --audit-level=high",
"audit:report": "mkdir -p .audit && npm audit --json > .audit/report.json || true",
"audit:fix": "npm audit fix && npm test",
"audit:force": "echo 'REVIEW REQUIRED: major version bumps will be applied' && npm audit fix --force && npm test",
"preinstall": "node -e \"if(process.env.CI) process.exit(0);\" || npm audit --audit-level=critical || true"
}
}
The preinstall hook checks for critical-severity vulnerabilities before every local install while silently skipping the check in CI (where npm ci handles it separately). The || true at the end means a critical finding prints a warning but does not abort the install. You can tighten this to abort instead by removing || true, but that can block developers from installing legitimate urgent dependency updates on machines that already have outstanding findings. Tune this to your team’s workflow.
Pair the scripts with a Dependabot configuration file that schedules weekly automated update PRs for all dependency types:
# .github/dependabot.yml
version: 2
updates:
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "weekly"
day: "monday"
time: "07:00"
open-pull-requests-limit: 10
groups:
# Group minor and patch updates together to reduce PR noise
minor-and-patch:
update-types:
- "minor"
- "patch"
# Security updates are always opened immediately, regardless of schedule
# (controlled by GitHub repository settings, not this file)
With this configuration, Dependabot opens security update PRs immediately when a new advisory is published and groups routine minor and patch updates into a single weekly PR. Your CI audit gate then validates each PR before it merges, giving you continuous coverage without manual intervention.
5 Common npm audit Pitfalls to Avoid
These mistakes appear repeatedly in security postmortems for Node.js projects. Each one is preventable with a one-time configuration change.
Pitfall 1: Not committing package-lock.json to version control
The most common Node.js security mistake is adding package-lock.json to .gitignore. The lockfile is what npm audit reads to determine your exact installed versions. Without it, the audit report is incomplete. More critically, every npm install on a new machine regenerates the lockfile from scratch and can pull in new vulnerable patch releases of packages declared with caret ranges in package.json. Always commit the lockfile and always use npm ci in CI, not npm install.
Pitfall 2: Treating all severity levels as equal blocking conditions in CI
Running npm audit --audit-level=low in CI and failing builds on every low-severity finding causes alert fatigue within two weeks. Developers learn to add || true to the audit command, skip it entirely, or merge to a different branch to bypass the gate. Use --audit-level=high for CI gates. Reserve low and moderate findings for weekly security reviews where a human applies context. Actionable gates that rarely fire get respected; noisy gates get disabled.
Pitfall 3: Running npm audit fix –force without running tests
--force can upgrade a package from version 3.x to 4.x without any warning other than the command output. If your code uses APIs removed in the new major version, the application silently fails at runtime because the missing function calls throw at call time, not at startup. Always run your full test suite immediately after --force. If test coverage is incomplete, do a manual smoke test of every feature that exercises the upgraded package before merging.
Pitfall 4: Ignoring “No fix available” results
When npm reports “No fix available,” most teams close the terminal and move on. This is acceptable for low and moderate findings, but critical and high “No fix available” results require one of the four manual remediation options from Step 10. “No fix available” from npm means no safe semver-compatible patch exists in the registry at that moment, not that the vulnerability is acceptable to ignore indefinitely. Check back weekly; new patch releases often appear within days of an advisory being published.
Pitfall 5: Using npm audit as a complete security program
npm audit only covers known CVEs in published packages with registered advisories. It cannot detect zero-day vulnerabilities, business logic flaws, insecure use of a legitimate API (for example, calling eval() with user-controlled input from a perfectly clean package), newly published malicious packages with no advisory yet, or packages with obfuscated payloads. npm audit is one layer in a security program. Pair it with static analysis tools (eslint-plugin-security, Semgrep), follow the Node.js security best practices and the OWASP npm Security Cheat Sheet, and conduct periodic manual code reviews focused on security-sensitive code paths.
8 Troubleshooting npm audit Problems
These are the error conditions and edge cases you will encounter on real projects, with specific remediation steps for each.
Problem 1: “ENOLOCK” or “0 packages audited”
Cause: No package-lock.json exists in the current directory. Solution: Run npm install first to generate the lockfile, then rerun npm audit. If package.json lists zero dependencies, the 0-package result is correct and is not an error. If you are in a monorepo, confirm you are in the right subdirectory or use npm audit --workspaces from the root.
Problem 2: Audit hangs with no output or times out
Cause: Network timeout reaching the npm registry. Check your configured registry with npm config get registry. If your organization uses a private registry proxy such as Nexus, Artifactory, or Verdaccio, it may not support the audit endpoint (/-/npm/v1/security/audits/quick). Contact your infrastructure team to either enable audit endpoint proxying or set HTTPS_PROXY and HTTP_PROXY environment variables. As a workaround, run npm audit --registry https://registry.npmjs.org to bypass the proxy for the audit call alone.
Problem 3: JSON parsing fails with “Cannot read property ‘severity’ of undefined”
Cause: Your parsing script was written for schema v2 but you are reading schema v1 output from npm 6. Schema v1 uses an advisories top-level key with numeric IDs. Schema v2 uses a vulnerabilities key with package names. Check report.auditReportVersion in the JSON: value 2 means schema v2; absent field means schema v1. Upgrade npm or adjust your parsing script to detect the schema version before accessing fields.
Problem 4: npm audit fix creates a broken node_modules state
Cause: Corrupted node_modules from a partial install or aborted npm command. Solution: Run rm -rf node_modules package-lock.json && npm install && npm audit fix. Starting from a clean install ensures the dependency resolver has a fully consistent starting state. Partial node_modules directories can cause npm to make incorrect assumptions during the fix calculation.
Problem 5: The same vulnerability reappears after every npm audit fix run
Cause: Multiple packages in your tree depend on the vulnerable package, and not all of them have a compatible fixed version available. The effects field in the JSON output lists every package that pulls in the vulnerable dependency. Update each of those parent packages separately using npm update <parent-package>, or apply an overrides entry as described in Step 10 to force the pinned fixed version across all dependents simultaneously.
Problem 6: “EAUDITNOPJSON” in CI
Cause: The CI job is running npm audit in a directory that does not contain a package.json. This usually happens when a CI step changes directories (for example, running in a temp directory after a build) or when a monorepo workflow accidentally runs audit from the root before workspace packages are initialized. Add an explicit working-directory to your GitHub Actions step, or verify that $GITHUB_WORKSPACE points to the correct project subdirectory.
Problem 7: npm audit reports zero vulnerabilities but Dependabot or Snyk reports high findings
Cause: The advisory is in Snyk’s proprietary database or has been submitted to the GitHub Advisory Database but not yet processed and published. Snyk and some security researchers often publish advisories earlier than the GitHub database because they have independent disclosure pipelines. This is a real coverage gap between tools. Cross-check findings via the GitHub Security Advisories search and the Node.js vulnerability blog. Running npm audit daily (via the scheduled CI job from Step 9) catches most findings within 24 to 48 hours of registry publication.
Problem 8: npm audit exits with code 1 in CI even though audit-level is set correctly
Cause: The audit-level set in your .npmrc file may be lower than expected, or the command-line flag is being overridden by the npm config cascade. npm evaluates configuration in this order from lowest to highest precedence: built-in defaults, global .npmrc, user-level .npmrc, project-level .npmrc, environment variables prefixed with npm_config_, and command-line flags. Run npm config get audit-level in your CI environment to see the resolved value. If a CI environment variable like npm_config_audit_level=low is set, it overrides your project .npmrc setting and causes every low finding to fail the build.
Advanced Tips for Production-Grade Audit Workflows
Audit npm Workspaces at Scale
If your project is a monorepo using npm workspaces, run npm audit --workspaces from the root to audit all workspace packages in a single pass. The JSON output includes a workspaces key that groups findings by workspace name, making it straightforward to route specific findings to the team responsible for each package. For very large monorepos with 20 or more workspace packages, consider running workspace audits in parallel using a custom script that spawns npm audit --workspace=packages/X for each package concurrently, then aggregates the JSON outputs:
# Audit all workspace packages and filter to critical/high only
npm audit --workspaces --json 2>/dev/null | node -e "
const chunks = [];
process.stdin.on('data', d => chunks.push(d));
process.stdin.on('end', () => {
const report = JSON.parse(Buffer.concat(chunks).toString());
const critical = report.metadata.vulnerabilities.critical;
const high = report.metadata.vulnerabilities.high;
console.log(\`Workspace audit: \${critical} critical, \${high} high\`);
process.exit(critical + high > 0 ? 1 : 0);
});
"
Harden Your npm Token and Registry Setup
npm audit only checks packages installed from the registry configured in .npmrc. If your private registry proxy does not mirror the advisory data from the public npm registry, you will miss findings for public packages installed through the proxy. Configure your proxy to pass through the audit endpoint (/-/npm/v1/security/audits/quick) as a live request rather than a cached response, so audit results always reflect current advisory data.
Rotate your npm publish token (the NPM_TOKEN secret in CI) on a quarterly schedule. A stolen npm token does not trigger a CVE because it is an account-level credential, not a package vulnerability. But it gives an attacker the ability to publish malicious versions of every package your account owns. A malicious patch release from your account would pass every downstream user’s npm audit check because the advisory database would not yet have a record of it. Token rotation combined with npm’s granular token scopes (read-only vs. automation tokens) limits the blast radius if a token is exposed.
Track Audit Results Over Time with a Simple Script
A single audit result is a snapshot. Tracking results over time reveals whether your vulnerability count is improving, stable, or growing. A simple approach stores dated JSON reports in a version-controlled directory and diffs them weekly:
# Run this as a weekly scheduled CI job and commit the output
mkdir -p .audit/history
DATE=$(date +%Y-%m-%d)
npm audit --json > .audit/history/${DATE}.json || true
# Extract and log a one-line summary for the changelog
node -e "
const r = require('./.audit/history/$(date +%Y-%m-%d).json');
const v = r.metadata.vulnerabilities;
console.log(\`\${new Date().toISOString().split('T')[0]} | C:\${v.critical} H:\${v.high} M:\${v.moderate} L:\${v.low} | total:\${v.total}\`);
" >> .audit/history/summary.log
git add .audit/history/ && git commit -m 'chore: weekly security audit snapshot' --allow-empty
Related Coverage
These articles from shattered.io extend the security practices covered in this tutorial:
- npm Supply Chain Attacks: 1.2M Malicious Packages [2026] – The broader threat landscape behind why npm audit exists and what it cannot catch
- OWASP Top 10 in Node.js: 12 Steps to Secure Your API [2026] – Code-level security controls that complement dependency scanning
- Node.js Crypto Module: 12 Steps, 30 Min [2026] – Implementing secure cryptography that audit cannot validate for you
- Rate Limiting in Node.js: 12 Steps, 30 Min [2026] – API protection layers that work alongside dependency security
- Node.js Session Management: 11 Steps, 30 Min [2026] – Session security controls paired with secure dependency practices
Frequently Asked Questions
Does npm audit slow down npm install?
Yes, by 1 to 5 seconds per install because it makes a network request to the npm registry audit endpoint. In CI where installs run frequently, disable it in .npmrc with audit=false and run npm audit as a dedicated, separate CI step. This preserves security coverage without adding latency to every install call.
What is the difference between npm audit and npm audit fix?
npm audit is read-only: it reports vulnerabilities and exits with code 0 (no vulnerabilities at or above the audit level) or code 1 (one or more vulnerabilities found). It does not change your package.json, package-lock.json, or node_modules. npm audit fix is a write operation: it updates the lockfile and installs patched versions. Always run npm audit first to understand what will change, then run npm audit fix to apply safe patches.
Is npm audit fix –force safe to run on a production branch?
No, not without a test-verified dedicated branch. --force applies major version bumps that can break APIs your application code depends on. Always run it on a branch named for the purpose (for example, deps/security-june-2026), execute your full test suite, manually verify critical code paths, and merge only after review. Never run --force directly on main or production.
How do I stop npm audit from failing CI on a vulnerability I cannot fix right now?
Set --audit-level to a threshold above the unfixable vulnerability’s severity. If the unfixable finding is moderate, use --audit-level=high to pass the gate while still catching critical and high findings. Document the deferred vulnerability in a security/vulnerability-exceptions.md file with the GHSA ID, the reason for deferral, and a next review date no more than 90 days out. This avoids disabling the gate while acknowledging the known risk.
What does “fix available via npm audit fix (semver-major)” mean?
It means a patched version exists, but it requires a major version bump such as moving from 3.x to 4.x. npm audit fix without --force will not apply this because major bumps may introduce breaking API changes that are outside the semver compatibility contract. You need to either add --force and test thoroughly, or manually update the package and migrate any breaking API changes in your application code.
Can npm audit detect malicious packages?
Only if the malicious package has already been reported and has a registered advisory in the GitHub Advisory Database. npm audit cannot detect newly published malicious packages, typosquatting attacks using names similar to popular packages, packages with obfuscated malicious code that have not yet been reported, or compromised maintainer accounts that have pushed a new version with a hidden payload. For supply-chain threat detection beyond known CVEs, add Snyk, Socket, or a similar tool that performs behavioral analysis on package code at install time.
How often should I run npm audit?
Run npm audit --omit=dev --audit-level=high on every pull request and push to main. Run a full npm audit including dev dependencies on a weekly schedule via CI. Run an additional manual audit any time you add, upgrade, or remove a dependency. New advisories are published every day, so a project that last passed audit on Monday can have a critical finding by Thursday with no code changes on your end. The weekly scheduled job is the safety net between active development cycles.
Does npm audit work with Yarn or pnpm?
No, npm audit reads package-lock.json specifically. Yarn uses its own audit command (yarn audit) and reads yarn.lock. pnpm uses pnpm audit and reads pnpm-lock.yaml. All three tools query the same underlying advisory database, so the findings are identical for the same dependency versions. Only the command and lockfile format differ. If your team uses mixed package managers across projects, use Snyk or Dependabot, which abstract over lockfile format and work consistently across npm, Yarn, and pnpm.




