npm 16 ๐ .npmignore vs .gitignore and files Field
Publishing a package to the npm registry involves a decision that is easy to get wrong: which files from the project directory end up in the published tarball. Three mechanisms compete for this decisionโ.gitignore, .npmignore, and the files field in package.jsonโand their precedence rules are not intuitive. A .gitignore entry that excludes a secrets file from version control does not necessarily exclude it from the published package. An .npmignore that overrides .gitignore can silently reintroduce a file that the developer thought was excluded. The files field inverts the logic entirely, treating the listed entries as the only files that should be published . The difference between these mechanisms is the difference between an allowlist and a blocklist, and that difference determines which failure mode a package is exposed to.
Key point: The files field is an allowlist and fails closed. .npmignore is a blocklist and fails open. When both are present, the files field takes precedence. When .npmignore is present, .gitignore is ignored for publishing. The only reliable way to confirm what will be published is npm pack --dry-run.
Why the three mechanisms exist
The version control problem. .gitignore tells Git which files should not be tracked. It exists because build artifacts, node_modules, environment files, and editor configuration should not be committed to the repository. The file is designed for the repository’s needs, not the registry’s needs. A file that is correctly ignored by Git can still be a legitimate part of the published packageโa compiled dist directory, for example, is often gitignored during development but must be published for consumers to use the package.
The registry problem. .npmignore tells npm which files should not be included in the published tarball. It was designed as the registry-side counterpart to .gitignore, with the same syntax and the same blocklist semantics. The problem is that the two files can drift: a developer adds an entry to .gitignore but forgets .npmignore, and the file that was excluded from the repository is still published to the registry . This drift is a security issue when the file contains credentials or internal configuration.
The allowlist problem. The files field in package.json reverses the logic. Instead of listing what to exclude, it lists what to include. Everything not listed is excluded by default. This means that a new file added to the project is not published unless it is explicitly added to the files array. The failure mode is a missing file in the published package, which is immediately visible to consumers, rather than a leaked file, which may not be noticed until it is exploited . GitHub’s own community discussion on the topic recommends files for this reason: “files in package.json is an allowlist, which is safer because new files are excluded by default” .
a. The Precedence Rules
The interaction between the three mechanisms follows a specific order. Understanding this order is the difference between a correct package and a leaked secret.
If files is present, it defines the allowlist. npm includes only the files and directories listed in files, plus a set of files that are always included regardless of the field. Those always-included files are package.json, the README, the LICENSE, and the file referenced by the main field . The files field takes precedence over both .npmignore and .gitignore. If a file is listed in files, it is published even if .npmignore excludes it. If a file is not listed in files, it is not published even if .npmignore would allow it.
If files is absent and .npmignore is present, .npmignore defines the blocklist. npm publishes everything in the project directory except what .npmignore lists, plus the always-included files. In this case, .gitignore is ignored entirely for the purpose of publishing . The presence of .npmignore is a signal to npm that the developer has taken explicit control over the registry contents, and the Git ignore file is not consulted.
If neither files nor .npmignore is present, .gitignore is used as the blocklist. npm falls back to the Git ignore file because it is the only signal available about what the developer considers disposable . This fallback is convenient for small packages that do not need separate ignore files, but it creates the drift problem: the .gitignore was written for Git, not for npm, and its exclusions may not match the registry’s needs.
The OWASP cheat sheet warns explicitly about the drift problem: “if a project is utilising both .npmignore and .gitignore files, everything which isn’t in .npmignore is published to the registry (i.e. the .npmignore file overrides the .gitignore). This condition is a common source of confusion and is a problem that can lead to leaking secrets” . A developer who adds a secrets file to .gitignore and assumes it will not be published is wrong if .npmignore exists and does not list the same file.
b. What Is Always Included and Always Excluded
Regardless of which mechanism is used, npm includes and excludes certain files automatically. The always-included files are package.json, the README (with any of the common extensions: .md, .txt, .rst), the LICENSE (or LICENCE), and the file referenced by the main field . These files are essential for the package to be usable: package.json defines the package, the README describes it, the LICENSE states the terms, and the main file is the entry point. Excluding them would produce a package that is broken or unusable.
The always-excluded files include .git, .npmrc, node_modules, and various lock files and configuration files that are specific to the development environment . The .git directory is excluded because it contains the repository history and would bloat the package. .npmrc is excluded because it can contain registry credentials or tokens. node_modules is excluded because dependencies are installed by the consumer, not bundled with the package.
The files field does not override the always-included files. A package with "files": ["dist"] still includes package.json, the README, the LICENSE, and the main file. The files field adds to the allowlist; it does not replace the built-in list.
c. Verifying the Tarball Contents
The only way to know what will be published is to ask npm to show the tarball contents without actually publishing. The npm pack --dry-run command performs the packaging step, applies all the inclusion and exclusion rules, and prints the resulting file list and size without writing the tarball or contacting the registry . The npm publish --dry-run command does the same and additionally shows what would be uploaded.
# Show exactly which files would be included in the tarball
npm pack --dry-run
# The output lists every file and the total package size
This verification step is not optional for packages that contain anything that should not be public. The precedence rules are complex enough that reasoning about them is error-prone. The dry run applies the rules mechanically and shows the result. A file that appears in the dry-run output will be published; a file that does not appear will not.
The files field can be configured interactively with a tool like create-include, which prompts for the files and directories to include and writes the resulting array to package.json . This is a convenience, not a substitute for verification. The dry run remains the authoritative check.
Complete Example Session
// ============================================
// PART 1: PROJECT DIRECTORY
// ============================================
// my-package/
// โโโ src/
// โ โโโ index.ts
// โโโ dist/
// โ โโโ index.js
// โโโ tests/
// โ โโโ index.test.ts
// โโโ .env
// โโโ .gitignore
// โโโ README.md
// โโโ LICENSE
// โโโ package.json
// ============================================
// PART 2: .GITIGNORE (FOR VERSION CONTROL)
// ============================================
// .gitignore
// node_modules/
// dist/
// .env
// *.log
// The dist/ directory is gitignored because it is a build artifact.
// The .env file is gitignored because it contains secrets.
// ============================================
// PART 3: THE PROBLEM WITH FALLING BACK TO .GITIGNORE
// ============================================
// If there is no .npmignore and no files field,
// npm uses .gitignore.
// This means dist/ is excluded from the published package.
// The package will be published without its compiled code.
// Consumers cannot use it.
// ============================================
// PART 4: .NPMIGNORE AS A BLOCKLIST
// ============================================
// .npmignore
// src/
// tests/
// .env
// *.log
// This publishes dist/, README, LICENSE, package.json.
// It excludes src/ (TypeScript source), tests/, .env.
// The .env is excluded because it is listed in .npmignore.
// ============================================
// PART 5: THE DRIFT PROBLEM
// ============================================
// Later, a developer adds a new secret file: .env.production
// They add it to .gitignore.
// They forget to add it to .npmignore.
// The file is excluded from Git but included in the package.
// This is the security failure that OWASP warns about.
// ============================================
// PART 6: THE FILES FIELD AS AN ALLOWLIST
// ============================================
// package.json
{
"name": "my-package",
"version": "1.0.0",
"main": "dist/index.js",
"files": [
"dist"
]
}
// Only dist/ is published, plus package.json, README, LICENSE,
// and the main file (dist/index.js).
// src/, tests/, .env, and .env.production are all excluded
// because they are not listed in files.
// A new file added to the project is not published unless
// it is explicitly added to files.
// ============================================
// PART 7: THE FILES FIELD TAKES PRECEDENCE
// ============================================
// Even if .npmignore exists and lists dist/,
// the files field includes dist/.
// The files field is the allowlist; it wins.
// ============================================
// PART 8: VERIFY WITH NPM PACK --DRY-RUN
// ============================================
npm pack --dry-run
// Output lists:
// package.json
// README.md
// LICENSE
// dist/index.js
// The .env and .env.production files are not listed.
// The tests/ directory is not listed.
// ============================================
// PART 9: WHAT IS ALWAYS INCLUDED
// ============================================
// Even with "files": ["dist"],
// npm always includes:
// - package.json
// - README.md
// - LICENSE
// - dist/index.js (the main file)
// These cannot be excluded by files or ignore files.
// ============================================
// PART 10: WHAT IS ALWAYS EXCLUDED
// ============================================
// npm always excludes:
// - .git/
// - .npmrc
// - node_modules/
// These are excluded regardless of files or ignore files.
The ten parts trace the three mechanisms and their precedence. Parts 2 and 3 show the fallback to .gitignore and its failure mode. Part 5 shows the drift problem that makes .npmignore unsafe for secrets. Part 6 shows the files field as the allowlist that prevents the drift.
Quick Reference
Precedence Rules
files Field | .npmignore | .gitignore | Effective Rule |
|---|---|---|---|
| Present | Ignored | Ignored | files is the allowlist |
| Absent | Present | Ignored | .npmignore is the blocklist |
| Absent | Absent | Present | .gitignore is the blocklist |
| Absent | Absent | Absent | Everything is published |
Always Included
| File | Reason |
|---|---|
package.json | Defines the package |
README (.md, .txt, .rst) | Describes the package |
| LICENSE / LICENCE | States the terms |
File in main field | Entry point |
Always Excluded
| File | Reason |
|---|---|
.git/ | Repository history |
.npmrc | May contain credentials |
node_modules/ | Installed by consumer |
Verification Commands
| Command | Purpose |
|---|---|
npm pack --dry-run | List files in tarball, no publish |
npm publish --dry-run | List files and publish target |
Best Practices
โ Do This:
// Use files as an allowlist
"files": ["dist"] // โ
# Verify the tarball contents before every publish
npm pack --dry-run # โ
# Include the compiled output in files
"files": ["dist", "bin"] # โ
# Use .npmignore only when the blocklist is small and stable
# and when files is not practical // โ
โ Don’t Do This:
# Rely on .gitignore for publishing exclusions
# It is ignored if .npmignore exists # โ
# Assume a file in .gitignore is not published
# Add it to files exclusion or .npmignore explicitly # โ
# Forget to rebuild dist/ before publishing
# files publishes the existing dist/ content # โ
# Publish without a dry run
# The tarball may contain secrets or missing files # โ
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
| Secrets published | .gitignore entry not in .npmignore | Use files allowlist |
Package missing dist/ | .gitignore fallback excluded it | Add files with dist |
files ignored | Typo in field name | Verify files is in package.json |
| README missing | Not a recognized extension | Use .md, .txt, or .rst |
.npmrc published | Cannot be excluded by files | Always excluded by npm |
Real-World Examples
1. Allowlist with Files
"files": ["dist"]
2. Multiple Directories
"files": ["dist", "bin", "types"]
3. Blocklist with .npmignore
src/
tests/
.env
4. Fallback to .gitignore
# No .npmignore, no files field
# .gitignore is used as the blocklist
5. Verify Tarball
npm pack --dry-run
6. Always Included: README
README.md is included even if not in files
7. Always Included: Main
"main": "dist/index.js" โ dist/index.js is included
8. Always Excluded: .npmrc
.npmrc is never published
9. Drift Scenario
.env added to .gitignore, not .npmignore โ published
10. Precedence Check
files present โ files wins, ignore files ignored
Visual
Precedence Decision Tree
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ IS "files" IN package.json? โ
โ โ
โ โโโโโโโโโโโโโโโโโโโ โ
โ โ YES โโโโโโบ Use files as allowlist โ
โ โ โ Ignore .npmignore and .gitignore โ
โ โโโโโโโโโโโโโโโโโโโ โ
โ โ
โ โโโโโโโโโโโโโโโโโโโ โ
โ โ NO โโโโโโบ Is .npmignore present? โ
โ โโโโโโโโโโฌโโโโโโโโโ โ
โ โ โ
โ โโโโโโโดโโโโโโ โ
โ โผ โผ โ
โ โโโโโโโโ โโโโโโโโ โ
โ โ YES โ โ NO โ โ
โ โ โ โ โ โ
โ โUse โ โUse โ โ
โ โ.npm โ โ.git โ โ
โ โignoreโ โignoreโ โ
โ โโโโโโโโ โโโโโโโโ โ
โ โ
โ The files field always wins. โ
โ .npmignore overrides .gitignore. โ
โ .gitignore is the fallback. โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Allowlist vs. Blocklist Failure Modes
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ ALLOWLIST (files) BLOCKLIST (.npmignore) โ
โ โ
โ โโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโ โ
โ โ New file added โ โ New file added โ โ
โ โ to project โ โ to project โ โ
โ โโโโโโโโโโฌโโโโโโโโโ โโโโโโโโโโฌโโโโโโโโโ โ
โ โผ โผ โ
โ โโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโ โ
โ โ NOT published โ โ PUBLISHED โ โ
โ โ (must be added โ โ (unless added โ โ
โ โ to files) โ โ to .npmignore) โ โ
โ โโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโ โ
โ โ
โ Failure mode: missing file Failure mode: leaked file โ
โ Visible to consumers May be invisible until โ
โ (package broken) exploited โ
โ โ
โ Allowlist fails closed. Blocklist fails open. โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
The Drift Problem
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ DEVELOPER ADDS .env.production โ
โ โ
โ โโโโโโโโโโโโโโโโโโโ โ
โ โ .gitignore โโโโโโบ .env.production added โ
โ โ โ Excluded from Git โ
โ โโโโโโโโโโฌโโโโโโโโโ โ
โ โผ โ
โ โโโโโโโโโโโโโโโโโโโ โ
โ โ .npmignore โโโโโโบ NOT updated โ
โ โ โ .env.production not listed โ
โ โโโโโโโโโโฌโโโโโโโโโ โ
โ โผ โ
โ โโโโโโโโโโโโโโโโโโโ โ
โ โ npm publish โโโโโโบ .env.production PUBLISHED โ
โ โ โ Secret leaked to registry โ
โ โโโโโโโโโโโโโโโโโโโ โ
โ โ
โ This is why files (allowlist) is safer. โ
โ A new file is not published unless explicitly listed. โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Always Included and Excluded
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ ALWAYS INCLUDED ALWAYS EXCLUDED โ
โ โ
โ โโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโ โ
โ โ package.json โ โ .git/ โ โ
โ โ README.md โ โ .npmrc โ โ
โ โ LICENSE โ โ node_modules/ โ โ
โ โ main file โ โ โ โ
โ โโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโ โ
โ โ
โ These rules apply regardless of files or ignore files. โ
โ The main file is included even if not in files. โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Summary
| Item | Value |
|---|---|
.gitignore | Git exclusion list; fallback for npm if no other file |
.npmignore | npm blocklist; overrides .gitignore |
files field | npm allowlist; overrides both ignore files |
| Precedence | files > .npmignore > .gitignore |
| Always included | package.json, README, LICENSE, main file |
| Always excluded | .git, .npmrc, node_modules |
| Verification | npm pack --dry-run |
Key takeaways:
filesis an allowlist;.npmignoreis a blocklist. The allowlist fails closedโa new file is not published unless it is added to the list. The blocklist fails openโa new file is published unless it is added to the ignore file. For security, the allowlist is the safer default .filestakes precedence over both ignore files. Iffilesis present,.npmignoreand.gitignoreare ignored for publishing. Iffilesis absent and.npmignoreis present,.gitignoreis ignored. The fallback to.gitignoreonly happens when neither of the other two mechanisms is present.- The drift between
.gitignoreand.npmignoreis a security risk. A file added to.gitignorebut not to.npmignoreis excluded from the repository but included in the published package. This is the failure mode that OWASP warns about, and it is the reasonfilesis recommended for packages that contain anything sensitive . - Certain files are always included and always excluded.
package.json, the README, the LICENSE, and themainfile are always published..git,.npmrc, andnode_modulesare never published. These rules apply regardless of the mechanism used. npm pack --dry-runis the authoritative verification. The precedence rules are complex enough that reasoning about them is unreliable. The dry run applies the rules mechanically and shows exactly which files will be in the tarball .
Remember: The choice between .npmignore and files is not a matter of preference. It is a choice between a blocklist that fails open and an allowlist that fails closed. The blocklist requires the developer to remember every file that should not be published, and to update the list every time a new sensitive file is added. The allowlist requires the developer to remember every file that should be published, and to update the list every time a new distributable file is added. The allowlist’s failure mode is a missing file, which is visible to consumers immediately. The blocklist’s failure mode is a leaked secret, which may not be visible until it is exploited. For packages that contain compiled output and source code, files is the safer and more maintainable choice.
Stop using slow, ad-bloated tool sites! ๐คฎ
๐ Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
โ
Finance (Mortgage, Interest, Inflation)
โ
Tech (Base64, JSON, Dev Suite, IP)
โ
Health (BMI, BMR, TDEE)
โ
Productivity (Timer, Workspace, QR)
โก๏ธ Fast & Private
๐ No data leaves your device
๐ 100% Free
๐ Use it now: https://tools.kandz.me
๐ Bookmark itโyouโll need it later!