Building a secure npm package comes down to two goals: making sure the code you publish is exactly the code you wrote, and making sure that code cannot become a weapon in someone else's dependency tree. Most published packages get both wrong by default. They ship far more files than they need, run with publish tokens that never expire, include lifecycle scripts nobody audits, and contain regular expressions that fall over on hostile input. The good news is that npm in 2026 gives maintainers strong, free tools to close every one of these gaps, and adopting them takes an afternoon.
The short answer: enable provenance via trusted publishing from CI, require two-factor authentication on your account and package, publish the minimum set of files, avoid postinstall scripts, commit a lockfile, and test your parsing code against catastrophic backtracking. The rest of this guide shows how.
Publish with provenance, not long-lived tokens
The single highest-leverage change is trusted publishing. Instead of storing a long-lived npm token in CI, you configure your package to accept publishes from a specific GitHub Actions or GitLab workflow using short-lived OIDC credentials. npm then generates a signed provenance attestation linking the published tarball to the exact commit and workflow that built it, verifiable on the package page.
# .github/workflows/publish.yml
permissions:
id-token: write # required for OIDC-based trusted publishing
contents: read
jobs:
publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
registry-url: https://registry.npmjs.org
- run: npm ci
- run: npm publish --provenance --access public
With this in place there is no token to steal, and consumers can verify the tarball came from your source. If you must use a token, use a granular access token scoped to a single package with a short expiry, never a classic "publish everything forever" token.
Publish the minimum, and disable install scripts
Every file you ship is attack surface and noise. Use the files allowlist in package.json so only build output is published, never tests, fixtures, .env files, or CI config.
{
"name": "@acme/widget",
"version": "1.2.0",
"files": ["dist/"],
"scripts": {
"build": "tsc -p tsconfig.build.json"
}
}
Run npm publish --dry-run and read the file list before every release; secrets leak into packages this way constantly. Do not ship a postinstall or preinstall script unless it is genuinely required, because install scripts are the primary mechanism malicious packages use to execute on a victim's machine. If your package needs no native build step, it should have no install lifecycle script at all.
Write code that survives hostile input
A package can be perfectly published and still be a liability. Regular expressions with nested quantifiers are the classic trap: CVE-2021-3807 was a regular-expression denial of service in ansi-regex, a dependency buried in thousands of trees, where a crafted string caused catastrophic backtracking and stalled the Node.js event loop (fixed in 5.0.1 and backports). If your package parses untrusted input, bound the input length, prefer linear-time patterns, and fuzz your parser.
// Reject oversized input BEFORE running an expensive regex.
export function parse(input) {
if (typeof input !== "string" || input.length > 4096) {
throw new TypeError("input too large");
}
return SAFE_PATTERN.exec(input);
}
Secure-package checklist
- Enable trusted publishing with provenance; avoid long-lived tokens.
- Require 2FA on your npm account and on the package publish setting.
- Restrict published files with a
filesallowlist and verify with--dry-run. - Remove unnecessary
postinstallandpreinstallscripts. - Commit a lockfile and run
npm auditin CI. - Cap input sizes and avoid regexes with catastrophic backtracking.
- Use a scoped name and publish with explicit
--access. - Pin CI actions by commit SHA, not by mutable tag.
How Safeguard helps
A secure package is only half the picture; you also depend on hundreds of packages you did not write. Safeguard's software composition analysis resolves your full dependency graph, flags vulnerable transitive packages like the ansi-regex ReDoS, and adds reachability so you fix what is actually exploitable first. When an upgrade is safe, autonomous auto-fix opens a tested pull request, and Griffin AI explains whether a flagged advisory reaches a real code path in your build. Maintainers run the same checks locally and in CI with the Safeguard CLI, and teams weighing tools can review the pricing.
Ship packages your users can trust: get started free or read the documentation.
Frequently Asked Questions
What is npm provenance and should I enable it?
Provenance is a signed attestation, generated when you publish with --provenance from a CI workflow using OIDC, that cryptographically links your published tarball to the exact source commit and build that produced it. Yes, enable it: it lets consumers verify your package was not tampered with and removes the need to store a stealable long-lived token.
Are npm install scripts a security risk?
They are the most common execution vector for malicious packages, because preinstall and postinstall run automatically on a victim's machine during npm install. Do not ship them unless your package truly needs a native build step, and audit any dependency that runs them.
How do I stop secrets from leaking into my published package?
Use the files allowlist in package.json so only build artifacts are published, and run npm publish --dry-run before every release to inspect the exact file list. This prevents .env files, test fixtures, and CI configuration from ending up in the tarball.
What is a ReDoS and how do I keep it out of my package?
A regular-expression denial of service occurs when a pattern with nested quantifiers backtracks catastrophically on crafted input, pinning a CPU core, as in CVE-2021-3807 in ansi-regex. Bound input length before matching, avoid nested quantifiers and ambiguous alternations, and fuzz any parser that touches untrusted strings.