Run npm audit --omit=dev --audit-level=high in the release job before the publish step. If it finds a runtime dependency issue at or above that severity, the command exits with a non-zero code and the job stops. Review the findings before you override it.
How the exit code works
The npm documentation says npm audit sends a description of your dependencies to the registry and reports known vulnerabilities. It exits with 0 when none are found. Otherwise the exit code depends on the audit-level setting, which accepts info, low, moderate, high, critical or none. The level sets the minimum severity that causes a failing exit. It does not filter the report, so all findings still appear in the output.
--omit=dev excludes development dependencies from the audit. By default the omit setting is dev when NODE_ENV is production. Omitted dependencies are still resolved into the lockfile, they just are not installed.
A release job
jobs:
release:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
- run: npm ci
- run: npm test
- run: npm audit --omit=dev --audit-level=high
- run: npm publish
Pin the actions to commit SHAs in a real workflow, as GitHub's hardening guide recommends. Keep publish credentials out of the earlier steps where possible, and give the job read permission by default.
npm ci matters here. It installs exactly what the lockfile says, never writes to it, and errors if the lockfile does not match package.json. The audit therefore runs on the tree you tested.
Caveats from the docs
| Caveat | What to do |
|---|---|
| A lockfile is required by default | Commit it. --no-package-lock can give different results each run |
| Some vulnerabilities cannot be fixed automatically | Plan for manual review |
npm audit fix --force can install semver-major updates | Avoid it without a plan |
| Meta-vulnerabilities arise from a dependency chain | A fix may need a range change in a direct dependency |
Deciding the threshold
Start at high if you have never gated before. A gate at low will fail often and teach people to bypass it. Move down a level once the backlog is clear.
Decide your override rule in advance. For example: a maintainer may skip the gate by adding a dated comment in the pull request that links the finding and says why it does not apply. Without a rule, the gate becomes the thing people disable at the worst moment.
What the gate does not cover
- New advisories published after release. Keep scanning.
- Findings in dev tools, which
--omit=devskips. Run a separate informational audit. - Malicious packages that have no advisory yet.
- Whether your code reaches the vulnerable function. The audit reports presence in the tree, so some findings will not matter.
Handling a failing gate
- Read the report and find the direct dependency that brings in the issue.
- Look for a fixed release and raise your range.
- If your code does not reach the flaw, record that and use your override rule.
- If nothing is fixed upstream, decide between waiting, replacing the dependency, or releasing with a note.
Try it yourself
Click through it at your own pace, or open it full screen. See how a policy gate stops a change and what it takes to clear it.
Quick check
Does the audit level hide lower severity findings?
No. It only sets which severity makes the command fail. All findings still print.
Why use npm ci before the audit?
It installs exactly the lockfile tree and fails on a mismatch, so you audit what you tested.
Safeguard policies and gates can block on reachable findings instead of every finding, which cuts false stops. Start with the free scan.
Sources
- npm docs, npm audit: https://docs.npmjs.com/cli/v10/commands/npm-audit
- npm docs, npm ci: https://docs.npmjs.com/cli/v10/commands/npm-ci
- GitHub docs, security hardening for GitHub Actions: https://docs.github.com/en/actions/security-for-github-actions/security-guides/security-hardening-for-github-actions
Last reviewed: 19 October 2026.