pubsafe
v1.1.0
Published
Check if sensitive files in your coding projects are properly gitignored
Maintainers
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 # LibraryCLI 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 3Output 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/ gitignoredProgrammatic 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: 8Default 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
