2025•Tools
pubsafe
← Back

pubsafe

CLI tool to check if sensitive files in your coding projects are properly gitignored.

Preview

Catch exposed secrets before npm publish

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 .gitignore entirely)
  • •If you have a files field 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.

bash
$ 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

Node.jsTypeScriptCLIInk

Impact

npm Downloads

340+

Focus

Security

Under the Hood

A peek at the implementation — the kind of code that powers pubsafe.

scanner.tstypescript
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}

Discussion

šŸ’¬ Be nice, or be funny. Preferably both.