
npm Will Never Publish Your .npmrc. It Will Publish Your .env.
Quick answer: npm maintains a hardcoded list of files it refuses to put in a tarball. .npmrc is on it, because that is where npm keeps its own credentials. .env is not on it. pnpm 12.8 (28 Sep 2026) added a warning — not a block, and only when your files field does not list the file. The actual fix is to publish from an allowlist and verify with --dry-run before every release.
Here is npm's always-ignored list, verbatim from the package.json reference: *.orig, .*.swp, .DS_Store, ._*, .git, .hg, .lock-wscript, .npmrc, .svn, .wafpickle-N, CVS, config.gypi, node_modules, npm-debug.log, package-lock.json, pnpm-lock.yaml, yarn.lock, bun.lockb.
Read it twice. There is a credential file on that list, and it is npm's. A subset of those entries is stronger still — the docs name .git, .npmrc, node_modules and the four lockfiles as files that cannot be included even if you match them with an explicit glob. npm has decided, at the level of hardcoded policy, that your registry token must never leave your machine. It has made no such decision about your database URL.
What pnpm 12.8 actually does
pnpm shipped the first move on this. From the 12.8 release notes, verbatim: "pnpm pack and pnpm publish now warn when the tarball includes a .env or .env.* file that the files field of package.json does not list."
Three things to notice about that sentence, because the exact wording is the whole feature.
It warns. It does not refuse. A warning in a release pipeline that nobody reads is a log line, not a control. If your publish runs in CI, the default outcome is that the file ships and the warning scrolls past.
It is conditional on files. If you explicitly list the file, pnpm takes you at your word and says nothing. That is the right call — an explicit allowlist entry is a decision — but it means the warning only fires for the accidental case, which is also the case where nobody is watching the output.
It covers two patterns. .env and .env.*. Not secrets.json, not config/local.yml, not a .pem, not a service-account JSON, not terraform.tfvars. The one filename convention that got a heuristic is the one everybody already knows to check.
Four clients, one warning between them
We checked each client's own documentation for this run rather than relying on reputation.
| Client | Documented .env warning | Dry-run flag |
|---|---|---|
| npm | None documented | --dry-run |
| pnpm 12.8+ | Yes, for .env / .env.* | --dry-run |
| Yarn | None documented | -n, --dry-run |
| Bun | None documented | --dry-run |
The npm pack reference describes what the command does and what --dry-run means and says nothing about file contents at all. Yarn's pack page lists four options and no content checks. Bun's publish docs describe --dry-run as running "the publish process without publishing the package, so you can verify what would be published" — which is exactly the right framing, and also an admission that verifying is your job.
To be precise about the claim: pnpm is the only one of the four whose documentation describes a warning. That is not the same as proving the others emit nothing. But if a safety check is not documented, you cannot build a release process on it.
The allowlist is the whole answer
There are two ways to control a tarball and they are not equivalent.
.npmignore is a denylist
Every file you did not think of ships. Add a build step that emits a new artifact, add a tool that drops a cache file, accept a contributor's PR that adds a fixture directory — each of those is included by default and silent. A denylist fails open, which is the wrong direction for a one-way operation.
files is an allowlist
Nothing ships unless you named it, with a handful of exceptions npm always adds: package.json, the README, the LICENSE or LICENCE in any case or extension, the file in main, and the files in bin. For most packages that means "files": ["dist"] and you are done. Set it and the pnpm warning becomes redundant, which is the point — a good default makes the heuristic unnecessary.
Pick one. npm's own developer guide frames them as alternatives — if .npmignore is "a maintenance headache, you might instead try populating the files property" — and documents no precedence rule for having both. So do not keep both and assume you know which wins. Add files, delete .npmignore, and verify the result with a dry run rather than reasoning about the interaction.
The lifecycle-script trap
Checking your working tree is not checking your tarball. prepack and prepublishOnly run before the tarball is assembled, which means the thing you inspect by hand and the thing that gets packed are different file sets. A build step that writes a sourcemap containing inlined sources, a codegen step that bakes a staging endpoint into a config, a test harness that writes fixtures next to the output — all of those appear only after the script runs.
This is the same category of blind spot as everything else that only exists in CI. The registry-side hardening of the last year — trusted publishing replacing long-lived tokens, lifecycle scripts moving to opt-in — all addresses who is allowed to publish and what runs on install. None of it addresses what is inside the bytes you publish. That part is still entirely on you.
A publish checklist that takes two minutes
Run npm pack --dry-run (or yarn pack -n, or bun publish --dry-run) and actually read the file list. Do it after your build, not before, so the lifecycle scripts have run. Set files to the narrowest set that works. Then add one CI step that packs the tarball, lists its contents, and fails the job if anything matches your own pattern set — not just .env, but *.pem, *.key, *.tfvars, *credentials* and whatever your stack actually names its secrets.
That step is ten lines and it fails closed, which is the property neither a denylist nor a warning has. If you want to eyeball a sourcemap or a generated config for an embedded URL before you trust it, a JSON formatter on the packed artifact is faster than reasoning about your build graph, and diffing two consecutive --dry-run file lists catches the release where something new silently joined the package.
The reason this matters more than it used to: a published version is immutable and already mirrored. Unpublishing does not recall it. PyPI has gone as far as rejecting new files on releases older than 14 days specifically to limit this blast radius. On npm, the window between publish and the first mirror fetch is your entire remediation budget. Rotate the credential, do not try to retract the tarball. More release-pipeline tooling is in cloud and DevOps, and the browser-based utilities above are all in free tools.
FAQ
Does npm exclude .env files automatically?
No. npm's documented always-ignored list includes .npmrc, .git, node_modules, the four lockfiles and a handful of editor and VCS artifacts, but it does not include .env or any .env.* variant. If a .env file is in your package directory and your files field does not exclude it, it ships.
Which files does npm always include?
package.json, the README, the LICENSE or LICENCE file, the file named in the main field, and the files named in the bin field. README and LICENSE can have any case and any extension. These are added regardless of what the files field says.
Should I use files or .npmignore?
Use the files field. It is an allowlist, so anything you have not named is excluded by default and new build outputs cannot silently join the tarball. .npmignore is a denylist and fails open. npm documents the two as alternatives rather than defining a precedence rule, so pick one, remove the other, and confirm the result with a dry-run pack.
Does pnpm block publishing a .env file?
No. pnpm 12.8 emits a warning when pnpm pack or pnpm publish would include a .env or .env.* file that the files field does not list. It is a warning, not a refusal, and it does not fire if the file is explicitly listed in files.
Can I unpublish a package that leaked a secret?
Treat the secret as compromised and rotate it immediately. A published version may already be mirrored, cached in CI, or pulled into lockfiles elsewhere, so removal from the registry does not recall the bytes. Rotation is the remediation; unpublishing is cleanup.
Why does checking my working tree not catch this?
Because prepack and prepublishOnly run before the tarball is assembled, so generated files that only exist after the build are in the package but not in the tree you inspected. Run a dry-run pack after the build and read the resulting file list, not the directory.
npm protects its own credential file by hardcoded policy. Yours needs an allowlist and a CI step that fails closed.

