pubsafe
CLI tool to check if sensitive files in your coding projects are properly gitignored.
Preview
Key Features
Smart Detection
Knows what files are sensitive ā .env, credentials, keys, databases ā and checks if npm will publish them.
Pre-Publish Safety
Run before npm publish to catch exposed secrets. Never wake up to 'secret detected' emails again.
Auto-Fix
pubsafe --fix automatically adds missing entries to your .npmignore.
Zero Config
npx pubsafe just works. No setup, no config files, no API keys.
CI/CD Ready
pubsafe --ci for automated pipelines. Non-zero exit on critical issues.
Clear Reports
Shows exactly what's exposed, what's safe, and what to do about it.
I almost published my Stripe API keys to npm.
It was 11 PM on a Friday. I'd been polishing a library for weeks. Tests passing, docs written, everything looking good. I ran npm publish, went to bed feeling accomplished.
The next morning: an email from GitHub. "A secret was detected in a public repository."
The key was in a .env.local file. A file I'd added to .gitignore. But here's the thing ā npm doesn't read .gitignore. It has its own rules, and those rules did not exclude my secrets.
I rotated the key immediately. Deprecated that npm version. Panicked for about an hour. Then I started building pubsafe.
The Problem Is More Common Than You'd Think
npm's file inclusion rules are... complicated:
- ā¢If you have a
.npmignore, it uses that (and ignores.gitignoreentirely) - ā¢If you have a
filesfield in package.json, it uses that as a whitelist - ā¢But it ALWAYS includes certain files like package.json
- ā¢And it has default exclusions for things like node_modules
Most developers don't know these rules. They assume .gitignore applies everywhere. It doesn't.
What pubsafe Does
It's simple: scan your project for sensitive files, check if they're properly excluded from npm, and warn you if they're not.
$ npx pubsafe
š Scanning for sensitive files...
šØ EXPOSED
ā .env ā NOT in .npmignore (will be published!)
ā config/secrets.json ā NOT in .npmignore (will be published!)
ā ļø WARNINGS
ā test/fixtures/users.json ā May contain PII
ā 3 sensitive fil...Built With
Impact
npm Downloads
340+
Focus
Security
Under the Hood
A peek at the implementation ā the kind of code that powers pubsafe.
1// The core scanning logic
2// Find sensitive files ā Check if npm will publish them ā Report
3
4interface Pattern {
5 glob: string;
6 name: string;
7 severity: 'critical' | 'warning' | 'info';
8}
9
10const SENSITIVE_PATTERNS: Pattern[] = [
11 // Critical - definitely secrets
12 { glob: '**/.env', name: 'Environment file', severity: 'critical' },
13 { glob: '**/.env.*', name: 'Environment file', severity: 'critical' },
14 { glob: '**/credentials*.json', name: 'Credentials', severity: 'critical' },
15 { glob: '**/secrets*.json', name: 'Secrets file', severity: 'critical' },
16 { glob: '**/*.pem', name: 'PEM key', severity: 'critical' },
17 { glob: '**/id_rsa*', name: 'SSH key', severity: 'critical' },
18
19 // Warnings - might be sensitive
20 { glob: '**/fixtures/**/*.json', name: 'Test fixture', severity: 'warning' },
21 { glob: '**/*.sqlite*', name: 'SQLite database', severity: 'warning' },
22 { glob: '**/*.log', name: 'Log file', severity: 'warning' },
23];
24
25async function scan(projectPath: string): Promise<ScanResult> {
26 const npmIgnore = await getNpmIgnoreRules(projectPath);
27 const result: ScanResult = { exposed: [], ignored: [], warnings: [] };
28
29 for (const pattern of SENSITIVE_PATTERNS) {
30 const matches = await glob(pattern.glob, { cwd: projectPath, dot: true });
31
32 for (const file of matches) {
33 const willPublish = !npmIgnore.ignores(file);
34
35 if (willPublish) {
36 if (pattern.severity === 'critical') {
37 result.exposed.push({ file, ...pattern });
38 } else {
39 result.warnings.push({ file, ...pattern });
40 }
41 } else {
42 result.ignored.push({ file, ...pattern });
43 }
44 }
45 }
46
47 return result;
48}
49
50async function getNpmIgnoreRules(projectPath: string): Promise<Ignore> {
51 const ig = ignore();
52
53 // npm's default exclusions
54 ig.add(['node_modules', '.git', '*.swp', '.DS_Store', 'npm-debug.log']);
55
56 // Check for .npmignore
57 const npmignorePath = join(projectPath, '.npmignore');
58 if (await exists(npmignorePath)) {
59 ig.add(await readFile(npmignorePath, 'utf-8'));
60 } else {
61 // Fall back to .gitignore if no .npmignore
62 const gitignorePath = join(projectPath, '.gitignore');
63 if (await exists(gitignorePath)) {
64 ig.add(await readFile(gitignorePath, 'utf-8'));
65 }
66 }
67
68 // Check for "files" field in package.json (whitelist mode)
69 const pkg = await readPackageJson(projectPath);
70 if (pkg?.files) {
71 // In whitelist mode, invert the logic
72 // Only files matching the whitelist are included
73 return createWhitelistIgnore(pkg.files);
74 }
75
76 return ig;
77}