npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

pubsafe

v1.1.0

Published

Check if sensitive files in your coding projects are properly gitignored

Readme

pubsafe

Pre-publish safety scanner that detects sensitive file leaks across distribution channels. Auto-detects active channels (git, npm, PyPI, Cargo), checks each channel's protection semantics, flags cross-channel inconsistencies, and suggests fixes.

Install

npm install -g pubsafe    # CLI
npm install pubsafe       # Library

CLI Usage

# Scan current directory
pubsafe .

# Scan with JSON output
pubsafe ~/Developer --json

# Interactive TUI
pubsafe --tui

# Scan specific directory with depth limit
pubsafe ./projects --depth 3

Output Example

  pubsafe v1.0.0 ── Sensitive File Audit
  Scanning: ~/Developer (found 3 projects)

  my-npm-package  ~/Developer/my-npm-package
  ─────────────────────────────────
  ✗ .env                  NOT gitignored
    ⚠ Also exposed in: npm
  ✓ .idea/                gitignored
    ⚠ Exposed in: npm
  ✓ node_modules/         gitignored

Programmatic API

pubsafe(directory, options?)

Scan a directory tree for projects with exposed sensitive files.

import { pubsafe } from 'pubsafe';

const result = await pubsafe('~/Developer');

for (const project of result.projects) {
  if (project.exposed.length > 0) {
    console.log(`${project.name}: ${project.exposed.length} exposed files`);

    for (const item of project.exposed) {
      console.log(`  ${item.pattern} (${item.category})`);

      // Per-channel exposure info
      if (item.channels) {
        for (const [channel, status] of Object.entries(item.channels)) {
          if (!status.protected) {
            console.log(`    ⚠ exposed in ${channel}`);
          }
        }
      }
    }
  }
}

Options

interface PubsafeOptions {
  patterns?: Array<{ pattern: string; category?: string; description?: string }>;
  exclude?: string[];       // directories to skip
  maxDepth?: number;        // project discovery depth (default: 5)
  concurrency?: number;     // parallel scan limit (default: 4)
  checkNpmignore?: boolean; // check npm channel (default: true)
}

Result Shape

interface PubsafeResult {
  projects: PubsafeProject[];
  totals: { projects: number; safe: number; exposed: number; missing: number };
}

interface PubsafeProject {
  name: string;
  path: string;
  isMonorepo: boolean;
  hasGitignore: boolean;
  activeChannels: string[];  // e.g. ['git', 'npm', 'cargo']
  exposed: PubsafeExposedItem[];
  protected: PubsafeProtectedItem[];
  missing: PubsafeMissingItem[];
  summary: { safe: number; exposed: number; missing: number };
}

interface PubsafeExposedItem {
  pattern: string;
  files: string[];
  category: string;
  description: string;
  channels?: Record<string, { protected: boolean }>;
  inconsistencies?: string[];  // e.g. ["exposed in npm"]
}

pubsafe.fix(projectPath, patterns, options?)

Fix exposed patterns by adding them to the appropriate ignore file.

// Dry run — preview what would change
const plan = await pubsafe.fix('/path/to/project', ['.env', '.vscode'], {
  dryRun: true,
});
console.log(plan.plannedContent);
console.log(plan.patternsAdded);   // ['.env', '.vscode']
console.log(plan.patternsSkipped); // patterns already present

// Apply the fix
const result = await pubsafe.fix('/path/to/project', ['.env', '.vscode']);
console.log(result.success);        // true
console.log(result.gitignorePath);   // '/path/to/project/.gitignore'
console.log(result.patternsAdded);   // ['.env', '.vscode']

Fix Options

interface PubsafeFixOptions {
  createGitignore?: boolean;  // create .gitignore if missing (default: true)
  dryRun?: boolean;           // preview only, don't write
  targetGitignore?: string;   // custom .gitignore path
}

Low-Level API

For deeper integration, use the scanner and channel APIs directly.

scan(directory, config, onProgress?)

Run the full scan pipeline with progress callbacks.

import { scan, loadConfig } from 'pubsafe';

const config = loadConfig(); // loads from .pubsafe.yml or defaults

const results = await scan('~/Developer', config, (progress) => {
  console.log(`Scanning ${progress.projectName} (${progress.current}/${progress.total})`);
});

for (const result of results) {
  console.log(result.projectName, result.activeChannels);
  for (const check of result.checks) {
    console.log(`  ${check.pattern.pattern}: ${check.status}`);
    console.log(`    git: ${check.isGitignored}`);
    console.log(`    npm: ${check.isNpmignored}`);
    console.log(`    cargo: ${check.isCargoExcluded}`);
    console.log(`    pypi: ${check.isPypiExcluded}`);
  }
}

Channel API

Each channel implements the Channel interface for checking file protection.

import {
  detectActiveChannels,
  GitChannel,
  NpmChannel,
  CargoChannel,
  PypiChannel,
} from 'pubsafe';
import type { Channel, ChannelId } from 'pubsafe';

// Auto-detect which channels apply to a project
const channels = await detectActiveChannels('/path/to/project', 'auto');
console.log(channels.map(c => c.id)); // ['git', 'npm']

// Use a specific channel
const npm = new NpmChannel();

if (await npm.detect('/path/to/project')) {
  const isProtected = await npm.isProtected('/path/to/project', '.env');
  console.log(`.env protected by npm: ${isProtected}`);

  // Batch check (more efficient — loads rules once)
  const results = await npm.isProtectedBatch('/path/to/project', [
    '.env', '.vscode', 'secrets.json'
  ]);
  for (const [file, protected_] of results) {
    console.log(`${file}: ${protected_ ? 'safe' : 'EXPOSED'}`);
  }
}

Channel Interface

interface Channel {
  readonly id: ChannelId;        // 'git' | 'npm' | 'cargo' | 'pypi'
  readonly name: string;
  readonly usesWhitelist: boolean;

  detect(projectPath: string): Promise<boolean>;
  isProtected(projectPath: string, relativePath: string): Promise<boolean>;
  isProtectedBatch(projectPath: string, relativePaths: string[]): Promise<Map<string, boolean>>;
  getIgnoreFilePath(projectPath: string): string;
  formatIgnorePattern(pattern: string): string | null;
}

Channel Protection Modes

Each channel uses a three-mode protection strategy:

| Channel | Whitelist Mode | Blacklist Mode | Fallback | |---------|---------------|----------------|----------| | npm | package.json files field | .npmignore | .gitignore | | Cargo | Cargo.toml include | Cargo.toml exclude | .gitignore | | PyPI | pyproject.toml include | MANIFEST.in / pyproject.toml exclude | .gitignore |

Fix Target Resolution

Determine which file to patch for each channel:

import { getFixTargetForNpm, getFixTargetForCargo, getFixTargetForPypi } from 'pubsafe';
import { NpmChannel, CargoChannel, PypiChannel } from 'pubsafe';

const projectPath = '/path/to/project';

// npm: returns .npmignore path, .gitignore path, or null (whitelist mode)
const npmTarget = await getFixTargetForNpm(projectPath, new NpmChannel());

// cargo: returns Cargo.toml path (if exclude exists) or null
const cargoTarget = await getFixTargetForCargo(projectPath, new CargoChannel());

// pypi: returns MANIFEST.in path (if setuptools + manifest exists) or null
const pypiTarget = await getFixTargetForPypi(projectPath, new PypiChannel());

Custom Patterns

Add your own sensitive file patterns:

import { pubsafe, DEFAULT_SENSITIVE_PATTERNS } from 'pubsafe';

const result = await pubsafe('.', {
  patterns: [
    ...DEFAULT_SENSITIVE_PATTERNS,
    { pattern: '.internal', category: 'custom', description: 'Internal configs' },
    { pattern: '*.secret', category: 'secrets', description: 'Secret files' },
  ],
});

Project Discovery

Find projects in a directory tree:

import { discoverProjects } from 'pubsafe';

const projects = await discoverProjects('~/Developer', 3); // maxDepth=3
for (const project of projects) {
  console.log(`${project.name} at ${project.path}`);
}

Workspace Resolution

Detect monorepo workspaces:

import { resolveWorkspaces } from 'pubsafe';

const workspace = await resolveWorkspaces('/path/to/monorepo');
console.log(workspace.type);     // 'npm' | 'pnpm' | 'lerna' | 'none'
console.log(workspace.packages); // ['/path/to/packages/a', '/path/to/packages/b']

JSON Output Schema

When using --json, the output follows this structure:

{
  "version": "1.0.0",
  "scannedDir": "/path/to/dir",
  "timestamp": "2026-01-23T00:00:00.000Z",
  "projects": [
    {
      "name": "my-project",
      "path": "/path/to/my-project",
      "isMonorepo": false,
      "hasGitignore": true,
      "activeChannels": ["git", "npm"],
      "checks": [
        {
          "pattern": ".env",
          "category": "secrets",
          "status": "exposed",
          "fileExists": true,
          "isGitignored": false,
          "matchedFiles": [".env"],
          "channels": {
            "npm": { "protected": false }
          }
        },
        {
          "pattern": ".idea",
          "category": "ide",
          "status": "safe",
          "fileExists": true,
          "isGitignored": true,
          "matchedFiles": [".idea"],
          "channels": {
            "npm": { "protected": false }
          },
          "inconsistencies": ["exposed in npm"]
        }
      ],
      "summary": { "safe": 1, "exposed": 1, "missing": 0 }
    }
  ],
  "totals": { "projects": 1, "safe": 1, "exposed": 1, "missing": 0 }
}

Configuration

Create .pubsafe.yml in your project root:

patterns:
  - pattern: ".internal"
    category: custom
    description: "Internal config files"

exclude:
  - vendor
  - third_party

maxDepth: 3
concurrency: 8

Default Patterns

pubsafe checks these sensitive file patterns by default:

| Category | Patterns | |----------|----------| | IDE | .vscode, .idea, .cursor, .fleet, .zed, .windsurf | | Secrets | .env, .env.local, .env.*.local, *.pem, *.key, credentials.json, secrets.json, .npmrc | | AI | .claude, .beads, agents | | System | .DS_Store, Thumbs.db, .history | | Dependencies | node_modules |

License

MIT