mailclad
v0.1.4
Published
Audit package-manager security settings and advisories across monorepos and folders of projects. Supports npm, pnpm, yarn, bun, uv, cargo, and bundler.
Maintainers
Readme
mailclad
mailclad walks a folder of repos, finds each package-manager root, and audits it. It does not write files unless you pass an apply flag.
npm, pnpm, yarn Berry, bun, uv, cargo, bundler, and composer all go through the same path. Yarn v1 and Poetry, pip, or Pipenv projects get flagged. They do not get a live audit.
How an audit runs
Each project gets a binary check, a settings read, then a vulnerability scan if the binary is there. Settings never call the package manager. The scan does.
flowchart TD
discover[Discover PM roots]
preflight[Check binary on PATH]
settings[Read settings files]
skip[Skip live audit]
live[Run native audit]
discover --> preflight
discover --> settings
preflight -->|missing| skip
preflight -->|found| liveDiscovery finds the roots. Preflight looks for npm, pnpm, yarn, bun, uv, cargo, bundle-audit, or composer on PATH. Settings then read that manager's committed config file.
A missing binary does not skip settings. You still get the file findings plus a pm.missing-binary warning. Only that manager's live audit command is skipped.
Leftover lockfiles and Poetry, pip, or Pipenv never need a binary for this step.
Install
From npm (requires Bun)
bun install -g mailclad
# or
npm install -g mailcladThe CLI runs on Bun, so Bun must be on your PATH either way.
Standalone binary (no Bun needed)
Grab the binary for your platform from the releases page:
mailclad-linux-x64/mailclad-linux-arm64mailclad-darwin-x64/mailclad-darwin-arm64(macOS)mailclad-windows-x64.exe
Fair warning: they're about 100 MB each, since Bun's runtime is baked in.
curl -fsSL -o mailclad https://github.com/jackmcpickle/package-manager-security/releases/latest/download/mailclad-darwin-arm64
chmod +x mailclad
./mailclad audit .Usage
mailclad audit [path] # audit only, never writes (default preset: standard)
mailclad audit . --preset strict # relaxed | standard | strict
mailclad audit . --apply # write settings fixes (clean git tree required)
mailclad audit . --fix # same as --apply
mailclad audit . --apply-advisories # upgrade packages with known fixes (no major bumps)
mailclad audit . -i # interactive: consent per repo
mailclad audit . --json # machine-readable output
mailclad audit . --sarif # SARIF output
mailclad audit . --report out.md # markdown reportExit code 0 means every project passed. 1 means a policy failure, either settings drift or an advisory at or above the preset's gate. 2 means the run was incomplete: a missing binary, a dirty tree blocked an apply, an audit subprocess died, or no projects were found.
Configuration lives in ~/.config/mailclad/config.toml, plus .mailclad.toml at the scan root or in any repo. The closer file wins, and flags win over files.
The default standard preset requires a 1-day release-age gate (minReleaseAgeDays: 1); strict requires 14 days, relaxed turns the gate off (0 days).
What it checks
Every manager gets the same four questions: are install scripts restricted, is there a release-age gate, is the lockfile present, and is the registry pinned. The settings behind those answers differ per manager:
| | install scripts | release-age gate | lockfile |
|---|---|---|---|
| npm | ignore-scripts, or allowScripts + strict-allow-scripts | min-release-age (days) | package-lock.json |
| pnpm | allowBuilds, dangerouslyAllowAllBuilds, strictDepBuilds | minimumReleaseAge (minutes) | pnpm-lock.yaml |
| yarn | enableScripts | npmMinimalAgeGate (minutes or 7d) | yarn.lock |
| bun | trustedDependencies | minimumReleaseAge (seconds) | bun.lock |
| uv | n/a | exclude-newer (date or "1 day") | uv.lock |
| cargo | n/a | minimum-release-age in .cargo/config.toml | Cargo.lock |
| bundler | n/a | BUNDLE_COOLDOWN in .bundle/config | Gemfile.lock |
| composer | allow-plugins must not be true | n/a (Composer has no cooldown yet) | composer.lock |
Also checked: pnpm blockExoticSubdeps, npm allow-git / allow-remote, yarn
checksumBehavior, enableStrictSsl and enableHardenedMode, and exclude
lists (minimumReleaseAgeExclude, npmPreapprovedPackages,
exclude-newer-package) that use a bare * and so void the gate.
Manager-specific checks beyond the baseline:
| Manager | Setting | What it enforces |
|---|---|---|
| npm | allow-file, allow-directory | must not be all (blocks local-path deps) |
| npm | allow-scripts-pin | must be true when scripts are restricted |
| npm | dangerously-allow-all-scripts | must not be true |
| pnpm | trust-policy | must be no-downgrade (pnpm ≥ 10.21) |
| pnpm | trust-lockfile | must not be true |
| pnpm | verify-deps-before-run | must be error (pnpm ≥ 10.12) |
| yarn | approvedGitRepositories | must block git-sourced deps (yarn ≥ 4.14) |
| uv | audit.malware-check | must be true (uv ≥ 0.11.31) |
| cargo | install.minimum-release-age | duration string meeting the preset (e.g. "1d") |
| bundler | BUNDLE_COOLDOWN | days ≥ preset minimum |
| composer | config.policy | advisories/malware blocking on; policy.advisories.audit must not be ignore |
| composer | secure-http / disable-tls | TLS required; HTTP repository URLs are reported but not rewritten |
| composer | source-fallback | must not be true (dist must not fall back to source) |
Checks are version-aware. pnpm 11 turns minimumReleaseAge on at 1440 minutes
and yarn defaults npmMinimalAgeGate to 1w and enableScripts to false, so
a missing key on those versions is reported as info ("you're relying on a safe
default") rather than high. mailclad reads the version from the packageManager
field in package.json; with no pin it assumes a current release.
Advisory audits
When settings pass, mailclad also scans lockfiles for known vulnerabilities at or
above the preset's advisory gate (high for standard, moderate for strict,
critical for relaxed). It shells out to each manager's native audit where
available:
| Manager | Command |
|---|---|
| npm | npm audit --json |
| pnpm | pnpm audit --json |
| yarn | yarn npm audit --json |
| bun | bun audit --json |
| uv | uv audit --output-format json --frozen |
| cargo | cargo audit |
| bundler | bundle-audit |
| composer | composer audit --format json --locked |
Poetry, pip, and Pipenv use OSV lookups instead of a native audit command.
Results are cached by lockfile digest; pass --refresh or --no-cache to bypass.
Development
Requires Bun >= 1.2.
bun install # install dependencies
bun test # run the test suiteCI runs on GitHub Actions (.github/workflows/ci.yml): tests with coverage, lint, and the coverage ratchet.
Build
bun run build # bundle to dist/mailclad.js (the npm bin, runs on Bun)
bun run build:binary # compile a standalone binary to dist/mailclad for this machineTo cross-compile for another platform:
bun build ./src/main.ts --compile --target=bun-linux-x64 --outfile dist/mailclad-linux-x64Targets: bun-linux-x64, bun-linux-arm64, bun-darwin-x64, bun-darwin-arm64, bun-windows-x64.
Releasing
Releases are automated from conventional commits on main.
.github/workflows/publish.yml— on push tomain(whensrc/orpackage.jsonchanges), runs tests, thencommit-and-tag-versionto bump the version, updateCHANGELOG.md, and push the release commit + tag. Release commits use[skip ci]so they don't re-trigger the workflow..github/workflows/release.yml— on tag push (v*), creates the GitHub release (notes fromCHANGELOG.md), attaches standalone binaries for all five platforms, and publishesmailcladto npm.
Use Conventional Commits so release notes are generated correctly:
feat: add uv exclude-newer check
fix(cli): handle missing lockfile in monorepos
feat!: drop yarn v1 support # major bumpTo cut a release manually:
bun run release # or: bun run release -- --release-as minor
git push --follow-tagsRepository secrets
| Secret | Used by | Purpose |
|---|---|---|
| DEPLOY_KEY | publish | SSH deploy key with bypass so the release commit/tag can push to protected main |
npm publishing uses OIDC trusted publishing — no NPM_TOKEN secret required. Ensure jackmcpickle/package-manager-security → .github/workflows/release.yml is configured as a trusted publisher for mailclad on npmjs.com.
License
MIT
