Safeguard
Supply Chain

How to Create an npm Package (and Publish It Securely)

A practical npm create package walkthrough: init, entry points, files whitelist, dry-run checks, then publishing with 2FA, provenance, and trusted publishing so your package cannot be hijacked.

Priya Mehta
Security Analyst
7 min read

To create an npm package you need three things: a valid package.json created with npm init, a defined entry point, and an npm account — but to publish one that stays trustworthy, you also need a files whitelist, two-factor auth, and a publish pipeline that cannot leak a token. Most guides on how to create npm package projects stop at npm publish and skip the part where packages get hijacked. Given that registry malware campaigns in 2025 spread specifically through stolen maintainer credentials and publish tokens, the security half is no longer optional reading.

Scaffold the Package

Start in an empty directory:

mkdir my-package && cd my-package
npm init

Answer the prompts, or use npm init -y and edit afterwards. Two early decisions matter more than they look:

Use a scoped name. Publish as @yourorg/my-package rather than a bare name. Scopes prevent dependency confusion attacks — nobody can register a lookalike inside a scope you control — and they signal ownership. Scoped packages are private by default, so public publishing needs an explicit flag later.

Declare your entry points deliberately. Modern packages should use the exports field, which locks down what consumers can import:

{
  "name": "@yourorg/my-package",
  "version": "0.1.0",
  "type": "module",
  "exports": {
    ".": {
      "import": "./dist/index.js",
      "types": "./dist/index.d.ts"
    }
  },
  "files": ["dist"],
  "engines": { "node": ">=18" }
}

The exports map means internal files are not part of your public API, so refactoring them is never a breaking change.

Control Exactly What Ships

The single most common packaging mistake is shipping files you never meant to publish: .env files, test fixtures with real data, CI configs, even .npmrc files containing tokens. Real credentials get leaked in published tarballs constantly.

Use the files allowlist in package.json — not .npmignore. An ignore file is a denylist that fails open: forget to add a new secret-bearing file and it ships. An allowlist fails closed.

Then verify before every release:

npm pack --dry-run

This prints the exact file list and unpacked size of the tarball that would be published. Make reviewing it a habit, and automate it: a CI step that diffs npm pack --dry-run output against a checked-in manifest catches accidental additions.

Also decide what your package does at install time. Avoid postinstall scripts unless genuinely necessary — install scripts are the primary execution vector for npm malware, and a growing number of consumers install with --ignore-scripts and will treat your hook as a red flag.

Version, Build, and Gate the Publish

Follow semver honestly: patch for fixes, minor for features, major for breaking changes. Wire your build into the publish lifecycle so you can never publish stale artifacts:

{
  "scripts": {
    "build": "tsc -p tsconfig.build.json",
    "prepublishOnly": "npm run build && npm test"
  }
}

prepublishOnly runs on npm publish and nowhere else — if the build or tests fail, the publish aborts. For the first public release of a scoped package:

npm publish --access public

Lock Down Your npm Account

Everything above is packaging hygiene. This section is what actually prevents the worst outcome — someone else publishing as you.

Enable 2FA now, before your package matters. In npm account settings, require two-factor authentication for login and publish. The September 2025 phishing wave that compromised maintainers of packages with billions of weekly downloads worked by stealing credentials; phishing-resistant 2FA (a passkey or hardware key, not SMS) is the control that breaks that chain.

Know the current token rules. npm's authentication model tightened significantly in late 2025: classic tokens can no longer be created and were revoked entirely on December 9, 2025, write-enabled granular tokens now default to a 7-day lifetime with a 90-day maximum, and interactive npm login sessions expire after two hours. If your publish automation still assumes an immortal token in a dotfile, it is already broken — see our companion piece on npm CLI login patterns for CI for the migration path.

Publish from CI with Trusted Publishing

The strongest current option removes long-lived tokens from the equation entirely. Trusted publishing, which npm supports for GitHub Actions and GitLab CI/CD, uses OIDC: your CI job proves its identity to the registry cryptographically, receives a short-lived credential scoped to that single publish, and nothing reusable ever exists to steal.

A minimal GitHub Actions release workflow:

name: release
on:
  push:
    tags: ["v*"]
permissions:
  id-token: write
  contents: read
jobs:
  publish:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          registry-url: "https://registry.npmjs.org"
      - run: npm ci
      - run: npm publish --provenance --access public

You configure the trusted publisher (repo, workflow file, environment) once in the package's settings on npmjs.com; no NODE_AUTH_TOKEN secret is needed.

The --provenance flag is worth the extra word: it attaches a signed, publicly verifiable attestation linking the published tarball to the exact source commit and CI run that built it. Consumers can then verify your package was built where you claim:

npm audit signatures

Think Like Your Own Consumer

Before announcing the package, evaluate it the way a cautious adopter would. Install it into a scratch project and check what actually arrives. Run a scanner against your own dependency tree — your package inherits the risk of everything in its dependencies, and shipping a vulnerable transitive dependency on day one is a bad first impression. This is the same review any software composition analysis tool performs on inbound packages; running one against your outbound package before release, as tools like Safeguard do in CI, closes the loop.

Finally, keep a minimal dependencies list. Every runtime dependency you add is a liability you are asking consumers to accept. Dev tooling belongs in devDependencies, which never ships.

Pre-Publish Checklist

  1. Scoped name, exports map, files allowlist set.
  2. npm pack --dry-run output reviewed — no secrets, no junk.
  3. No install scripts, or a documented reason for them.
  4. 2FA enforced on the account; no classic-token-era automation remaining.
  5. Publishing via trusted publishing (or a short-lived granular token) with --provenance.
  6. prepublishOnly gates build and tests.
  7. Dependency tree scanned clean.

FAQ

What is the fastest way to create an npm package?

npm init -y in an empty directory gives you a publishable package.json in seconds. The parts worth slowing down for are the files allowlist, the exports map, and account security — those are hard to retrofit after you have consumers.

Do I need to publish to try my package locally?

No. Use npm link for live development against another project, or npm pack to produce the real tarball and install it directly with npm install ./yourorg-my-package-0.1.0.tgz. The tarball route is better for final verification because it goes through the exact same packing rules as publish.

What does the --provenance flag do?

It publishes a signed attestation connecting the tarball to the source repository, commit, and CI workflow that built it. Anyone can verify the linkage with npm audit signatures, which makes silently substituted or manually built releases detectable.

Should my package use a postinstall script?

Almost certainly not. Install scripts execute arbitrary code on every consumer's machine, they are the main delivery mechanism for npm malware, and many organizations block them wholesale. If you need native compilation, prebuilt binaries with a fallback are the friendlier pattern.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.