Skip to content

Build1 publisher3 min readPublished

Adding .npmignore to drop test fixtures revoked a year of .gitignore exclusions

npm builds its tarball from the first ignore source it finds, so a .npmignore added to shrink a package stops .gitignore being consulted and the env file it was hiding ships to the registry.

The Engineer · Build desk

Illustration accompanying Adding .npmignore to drop test fixtures revoked a year of .gitignore exclusions

What happened

  • npm checks three ignore sources in a fixed order, the files array in package.json, then .npmignore, then .gitignore, and stops at the first one it finds.
  • In a six-command reproduction, adding a one-line .npmignore containing test/ removed the fixtures from the pack list and added .env.local to it.
  • The author's recommendation is the files whitelist in package.json plus an assertion on tarball contents in CI.

Compiled by The EngineerSomething wrong?How this is made

Why it matters

  • exposure Whatever lands in the tarball is the byte-for-byte set every consumer downloads, so a secret that slips past the pack step is distributed, not merely committed.
  • decision Teams now choose between two different failure modes: a blacklist that can be voided by one new file, or a whitelist whose omissions leave files out of the package.
  • constraint Auditing the repository root is not enough, because an ignore file anywhere in the tree changes what ships from that subtree alone.
  • cost The check costs one command per release and uploads nothing.

The override is per directory, not per package. An `.npmignore` sitting in `src/` governs `src/`, while a sibling directory without one still falls back to its own `.gitignore` [8]. npm finds both files by name rather than asking git, so the behaviour reproduces in a directory that was never a repository [11]. The documentation says npm will use `.gitignore` when there is no `.npmignore` file [3]. Read quickly, that sounds like npm consults both.

The path into the leak is a cleanup commit. A package ships its fixtures, the tarball is 4 MB, someone adds `.npmignore` with `test/` in it, and the tarball drops to 300 KB [17]. That is roughly a thirteenfold reduction [1]. The diff does exactly what it says it does, and npm emits no warning that the same commit stopped `.gitignore` from being read [1]. In the account published on dev.to, the list that came out the other side was 41 files, two of them a local env file and a `scripts/notes.md` holding an internal hostname, both ignored by git for a year [4].

npm's own always-excluded list is thirteen entries [2]: `.git`, `CVS`, `.svn`, `.hg`, `.DS_Store`, `._*`, `.*.swp`, `npm-debug.log`, `.npmrc`, `node_modules`, `config.gypi`, `*.orig` and `package-lock.json` [5]. Env files of any flavour, `*.pem`, `*.key` and `terraform.tfvars` are not among them [6]. A second short list beats your ignore rules in the other direction and always ships: `package.json`, `README`, `LICENSE` or `LICENCE`, and whatever file the `main` field points at [7].

Checking costs one command. `npm pack --dry-run` and `npm publish --dry-run` both print the file list and upload nothing [12]. For the bytes instead of a printed summary, `tar -tf "$(npm pack --silent)"` opens the real tarball, with every path prefixed `package/`, and a consumer gets that set [13]. `npm pack` also runs your `prepack` and `prepare` scripts, so anything they generate appears in the list [14]. At publish time npm prints the total file count and unpacked size, which makes a jump from 12 files to 41 between releases a visible event [15].

I would use the `files` array for this, and in my context the reason is ordering: `files` is the first source npm checks, so it cannot be shadowed by a stray ignore file three directories down [2]. It is a whitelist, so an omission leaves a file out of the tarball instead of putting one in. The author of the post recommends pairing it with a CI assertion on tarball contents [16].

The evidence here is one team's package and a six-command reproduction that anyone can run in an empty directory [9][10]. The post does not say how many published packages carry both ignore files, so the size of the exposed population is unknown. The ordering rule is documented and the demo is deterministic. The failure needs no mistake to trigger.

What to watch

  • A warning in npm's publish output when a .npmignore shadows .gitignore rules would close this without any repo change.
  • An audit that counts .npmignore files in subdirectories, not just package roots, would show how wide the per-directory override reaches.
  • Registry-side scanning of published tarballs for env files and private keys would catch the leak after the fact.
Loading claim ledger
Loading source directory links
Loading share composer
Loading topic controls
Loading related stories