How to write a secure npm package
Most supply chain security content is written for the consumers of packages — the developers who run npm install. Less is written for the publishers. If you maintain an npm package, you're part of the supply chain. Here's how to do it securely.
Protect your npm account
Enable 2FA on publish. This is the single most important thing a maintainer can do. Every major compromised-maintainer attack that used account takeover would have been blocked by 2FA on publish operations.
# Enable 2FA on your npm account
npm profile enable-2fa auth-and-writes
The auth-and-writes option requires 2FA for both login and publishing. This means an attacker with your password still can't publish a malicious version without your second factor.
Audit your npm tokens. If you have long-lived npm tokens configured in CI environments, audit them:
npm token list
Revoke any tokens that are no longer needed. Granular tokens (scoped to specific packages, with expiry dates) are far safer than long-lived automation tokens.
Use npm's granular access tokens. Modern npm access tokens support:
- Package scope (token can only publish specific packages)
- Read-only vs read-write
- IP allowlist (token only works from specific IP ranges)
- Expiry date
Use all of these. A CI token that can only publish my-package, only from GitHub Actions IP ranges, and expires in 90 days is vastly harder to abuse than a token with full account access and no expiry.
Secure your publishing pipeline
Publish from CI only, never from a developer machine.
# .github/workflows/publish.yml
name: Publish
on:
release:
types: [published]
permissions:
contents: read
id-token: write # required for npm provenance
jobs:
publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: '20'
registry-url: 'https://registry.npmjs.org'
- run: npm ci
- run: npm publish --provenance --access public
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
The --provenance flag generates a Sigstore attestation linking this publish to the GitHub Actions run and the source commit.
Lock your package's publish settings
# Require 2FA to publish this specific package
npm access 2fa-required <package-name>
# Set a publish allowlist (only specific accounts can publish)
npm team create <your-scope>:publishers
npm team add <your-scope>:publishers <your-username>
npm access grant read-write <your-scope>:publishers <package-name>
Avoid patterns that trigger security scanners
Security-conscious users run tools like Veln, Socket.dev, and others that flag suspicious patterns. Even if your code is legitimate, these patterns will trigger false positives that cause friction for your users:
eval()with non-literal arguments — if you need dynamic evaluation, document it in your README and explain the use caseBuffer.from(string, 'base64')followed byeval()orexec()— use this pattern legitimately only when absolutely necessary- Outbound HTTP requests in
postinstallscripts — document this explicitly; explain what data is sent and why process.envreads for credential-adjacent variable names in install scripts — avoid this entirely; read environment variables only at runtime in application code, not at install time
Document your install hooks
If your package legitimately uses postinstall (for native compilation, binary downloads, etc.), be explicit about it:
{
"scripts": {
"postinstall": "node scripts/download-binary.js"
}
}
And in your README:
Installation notes: This package downloads a pre-built binary during
npm install. The binary is downloaded fromhttps://github.com/your-repo/releasesand verified against a SHA-256 checksum. No data is sent to any external service. If you need to audit the download script, seescripts/download-binary.js.
Transparency about install-time behavior builds trust and reduces unnecessary friction from security tools.
Monitor for account security events
Enable npm email notifications for all publish events on your account. If you receive a notification about a publish you didn't make, your account has been compromised and you need to respond immediately.
npm whoami # verify your account
npm access ls-packages # see all packages you can publish
npm token list # check for unauthorized tokens
If you publish npm packages, you're part of the supply chain. Secure your account, publish from CI with provenance, and document your install hooks.