SafeInstall
Documentation

SafeInstall

Package safeinstall-cli (0.10.0). Local-first wrapper: policy is evaluated before npm, pnpm, or bun runs. Blocks risky installs by default and, opt-in, verifies the cryptographic provenance of registry packages via Sigstore trusted publisher pinning. Not a CVE scanner.

Overview

Prefix your package manager with safeinstall. The CLI resolves what would be installed, fetches registry metadata from the configured npm-compatible registry when needed (public npm registry by default), evaluates rules, then either exits with code 2 (blocked) or invokes the real tool—often with defaults such as ignore-scripts.

SafeInstall does not proxy the registry or modify tarballs. Network access is required to evaluate registry packages (publish time and declared scripts).

Local-first
Policy runs on your machine.
Lockfile-aware
npm/pnpm project installs use package-lock / pnpm-lock.
Fail-closed
Ambiguous or mismatched lockfile / packageManager states block.
Explicit outcomes
Exit 2 means blocked by policy.
Monorepo-safe
Workspace-wide flags blocked; target one package with -C / --prefix.
Honest scope
Not malware detection; transitive policy not complete.
Cryptographically verifiable
Registry packages can be verified against their Sigstore attestation and pinned to a specific owner/repo, refusing valid signatures from unexpected sources.
Typo-squat aware
Requested names are matched against a curated top-package list via Damerau-Levenshtein distance. Catches lodsh, raect, axois, and friends before they install.
Transitive aware
Enable transitive.mode to walk the full lockfile tree. Flags install scripts and untrusted sources in the transitive dependency graph with zero registry calls.
Continuity-aware
Enable continuity.mode to learn a per-package provenance baseline and block attestation downgrades or source-repository changes between versions — without the optional sigstore package.
Agent-ready
Run safeinstall mcp to expose the same engine to AI coding agents over MCP, so check_package is consulted before any install.
Agent-enforced
safeinstall guard install hooks Claude Code and Cursor at the shell layer: raw installs are denied with the SafeInstall-routed command, package runners require approval.
Self-defending
safeinstall trust lock baselines the files that configure SafeInstall and your agent, so an injected agent cannot quietly rewrite the rules — re-verified in CI on every pull request.

Installation

  1. Use Node.js >=20.
  2. Install globally or invoke with npx — no account or registry auth required.
$npm install -g safeinstall-cli
# Without a global install:
$npx safeinstall-cli check

Open source · MIT licensed · Source on GitHub

Usage

Pass through the same arguments you would use with the underlying tool. Since 0.9.0 the documented package-manager aliases work too — npm i, pnpm i, bun a, npm clean-install.

$safeinstall pnpm add axios
$safeinstall npm i axios
$safeinstall npm install
$safeinstall bun add zod
$safeinstall check
$safeinstall init
$safeinstall guard install
$safeinstall trust lock --ci github
$safeinstall --json pnpm add axios
$safeinstall --config ./ci/safeinstall.config.json check

Project installs

For pnpm install and npm install / npm ci, direct dependency versions come from pnpm-lock.yaml or package-lock.json (or shrinkwrap)—not loose ranges in package.json alone. Stale, missing, or mismatched lockfile entries fail closed instead of guessing.

If packageManager is set in package.json, using a different CLI is blocked. Workspace-targeting flags that may touch multiple packages (e.g. pnpm --filter, npm --workspace) are blocked; use -C or --prefix for a single package directory.

bun install still uses manifest-oriented analysis; full lockfile parity with npm/pnpm is not implemented yet.

Policy (defaults)

These are the main gates. Adjust or exempt via config where documented below.

Release ageBlocked: release too new

Registry packages must be at least 72 hours old by default (minimumReleaseAgeHours).

Lifecycle scriptsBlocked: install script present

preinstall, install, and postinstall on the resolved registry version are blocked unless allowed per package via allowedScripts.

SourcesBlocked: untrusted source

allowedSources defaults to registry, workspace, file, and directory. Git, tarball, and URL installs are outside the default set.

Trust downgradeBlocked: trust level dropped

Blocks when a dependency moves from registry to git/url/tarball, or when a new registry version adds lifecycle scripts that the version already in node_modules did not declare (comparison needs that prior install present).

Typo-squat detectionBlocked: suspected typo-squat

Requested package names compared via Damerau-Levenshtein distance with transposition against a curated top-package list. Three modes: off (default), warn, block. Configure via typoSquat.mode. See Configuration.

Provenance verificationBlocked: publisher mismatch / attestation missing / attestation invalid / attestation unreachable

Cryptographically verifies Sigstore attestations for registry packages via the public Sigstore trust root and Rekor transparency log. Pins source repository via provenance.trustedPublishers. Three modes: off (default), warn, require. Configure via provenance.mode. See Configuration. The sigstore package is an optional dependency as of 0.6.0. It is only installed when your package manager resolves optional dependencies (npm does by default, pnpm does not unless configured). If you enable provenance.mode and sigstore is not available, SafeInstall will show an error with install instructions.

Transitive dependenciesBlocked: transitive install script / transitive untrusted source

Walks the full lockfile tree and flags transitive packages that declare lifecycle scripts (npm only — pnpm lockfiles do not record install-script presence) or resolve from untrusted sources (npm and pnpm). Three modes: off (default), warn, block. Configure via transitive.mode. See Configuration.

Provenance continuityBlocked: provenance downgrade / identity discontinuity

Learns a per-package trust baseline from the provenance identity of recent versions and blocks deviations npm itself does not: provenance-downgrade (recent versions were attested, this one is not) and identity-discontinuity (attested from a different source repository than the baseline). Reads npm's published attestation metadata, so it works without the optional sigstore package. Packages with no provenance history have no baseline and stay silent. Three modes: off (default), warn, block. Configure via continuity.mode. See Configuration.

Configuration

Optional safeinstall.config.json is discovered by walking upward from the effective project directory; the nearest file wins. Since 0.9.0 you can also pass an explicit path with the global --config <path> flag — an explicit path that cannot be read is a hard error (exit 1), never a silent fallback to defaults, so CI cannot accidentally run with a weaker policy than intended.

safeinstall.config.json (shape)
{
"minimumReleaseAgeHours": 72,
"registryUrl": "https://registry.npmjs.org",
"allowedScripts": {
"esbuild": ["postinstall"]
},
"allowedSources": ["registry", "workspace", "file", "directory"],
"allowedPackages": [],
"packageManagerDefaults": {
"npm": { "ignoreScripts": true },
"pnpm": { "ignoreScripts": true },
"bun": { "ignoreScripts": true }
},
"typoSquat": {
"mode": "warn",
"minNameLength": 4,
"ignore": []
},
"provenance": {
"mode": "warn",
"requireFor": [],
"trustedPublishers": {
"axios": "axios/axios"
},
"offlineBehavior": "fail-closed"
},
"transitive": {
"mode": "warn",
"checks": ["install-script", "untrusted-source"]
},
"continuity": {
"mode": "warn",
"baselineSize": 5
}
}
FieldRole
minimumReleaseAgeHoursMinimum age in hours for registry versions.
registryUrlnpm-compatible registry URL for metadata (mirrors, Artifactory, Verdaccio).
allowedScriptsPackage name → allowed script names. Does not remove ignore-scripts unless you change packageManagerDefaults.
allowedSourcesSource types permitted (add git, tarball, url only if you intend to allow them).
allowedPackagesNames that skip release-age, install-script, and typo-squat checks (with warning). Source, trust-downgrade, and provenance checks still apply. Changed in 0.4.0.
packageManagerDefaultsPer-manager flags forwarded when spawning the tool; ignoreScripts defaults to true.
typoSquat.mode"off" / "warn" / "block". Controls the typo-squat check. Defaults to "off".
typoSquat.minNameLengthMinimum length of a requested package name to participate in the typo-squat check. Names shorter than this are skipped. Defaults to 4.
typoSquat.ignoreArray of package names (lowercased on load) that will never be flagged as typo-squats. Use for known legitimate lookalikes.
provenance.mode"off" / "warn" / "require". Controls Sigstore provenance verification for registry packages. Defaults to "off".
provenance.requireForArray of package name patterns (exact or glob with * wildcard). Provenance is required for these packages even when mode is "warn".
provenance.trustedPublishersObject mapping package name patterns to expected owner/repo slug patterns. Example: { "axios": "axios/axios", "@acme/*": "acme/*" }. A verified attestation whose source repository does not match the pin is ALWAYS blocked, even in "warn" mode.
provenance.offlineBehavior"fail-closed" (default) or "allow-cached". Controls what happens when the attestation endpoint is unreachable.
transitive.mode"off" / "warn" / "block". Evaluate transitive dependencies from the lockfile. Defaults to "off".
transitive.checksArray of checks to run transitively: "install-script" and/or "untrusted-source". Defaults to both.
continuity.mode"off" / "warn" / "block". Learn a per-package provenance baseline and block downgrades or identity changes between versions. Defaults to "off".
continuity.baselineSizeNumber of recent versions sampled to learn the provenance baseline. Defaults to 5.

CI and exit codes

0 — success or policy allowed. 1 — runtime or config error. 2 — blocked by policy. Use 2 like any other failing step in a pipeline.

GitHub Actions (illustrative)
- run: safeinstall pnpm install --frozen-lockfile
env:
CI: true

safeinstall check

Evaluates direct dependencies only. On success, stderr includes: Check passed: no direct dependency policy violations found.

$safeinstall check --json

safeinstall init

Writes a starter safeinstall.config.json in the current directory. Refuses if the file exists unless you pass --force.

$safeinstall init

JSON output

Put --json anywhere in argv. Structured output goes to stdout; human-oriented messages stay separate. Field details are stable in the CLI version you install.

$safeinstall --json pnpm add axios

GitHub Action

SafeInstall is available as a reusable GitHub Action at Mickdownunder/SafeInstall@v1. The action is a composite action that installs safeinstall-cli and runs policy checks. No Docker image or custom Node runtime is required.

Basic usage

workflow step
- uses: Mickdownunder/SafeInstall@v1

By default the action runs safeinstall check --json against direct dependencies. The job fails if any dependency is blocked by policy.

Install mode

workflow step
- uses: Mickdownunder/SafeInstall@v1
with:
mode: install
package-manager: pnpm
args: "--frozen-lockfile"

In install mode the action runs the package manager through SafeInstall so policy is enforced before packages are written to disk.

Inputs

InputDefaultDescription
modecheck"check" audits deps; "install" runs the PM through SafeInstall.
package-managerpnpmnpm, pnpm, or bun (install mode only).
argsAdditional arguments forwarded to the package manager.
config-pathExplicit path to safeinstall.config.json.
versionlatestSafeInstall CLI version to install.

Outputs

OutputDescription
decisionallow or block.
summaryHuman-readable one-line summary.
exit-code0 (allow), 2 (block), 1 (error).
jsonFull JSON result from SafeInstall.

Job summary

The action writes a job summary to the GitHub Actions UI. Allowed runs show a green checkmark and the summary line. Blocked runs show the full JSON output in a collapsible details block so reviewers can see exactly what was blocked and why without clicking into the logs.

MCP server / AI agents

The CLI protects people who type safeinstall. The MCP server reaches the AI coding agent itself — Claude Code, Claude Desktop, Cursor, Windsurf, Cline — so the policy engine is consulted before an agent suggests or runs an install, without anyone typing the prefix.

safeinstall mcp starts a Model Context Protocol server over stdio that exposes one tool, check_package, backed by the exact same engine as the CLI (release age, install scripts, untrusted sources, typo-squat, Sigstore provenance, and provenance continuity).

Connect your client

Add the server to your MCP config — .mcp.json / claude_desktop_config.json for Claude Code/Desktop, .cursor/mcp.json for Cursor. Windsurf, Cline, and other MCP clients use the same shape.

MCP server config
{
"mcpServers": {
"safeinstall": {
"command": "npx",
"args": ["safeinstall-cli", "mcp"]
}
}
}

If SafeInstall is installed globally you can instead use "command": "safeinstall", "args": ["mcp"].

MCP tools are advisory. Adding the server makes check_package available; it does not force the agent to call it. Paste the rule from mcp/agent-rule.md into your CLAUDE.md / Cursor Rules to make the agent call it before every install. That rule is what turns "available" into "always consulted". For enforcement that does not depend on the agent cooperating, add the agent guard.

check_package

Accepts name (required), version (optional, defaults to latest), and manager (optional, informational), and returns a JSON verdict:

check_package verdict (shape)
{
"verdict": "allow" | "block",
"name": "...",
"version": "...resolved...",
"reasons": [ { "code": "...", "message": "...", "suggestion": "..." } ],
"warnings": ["..."],
"infos": ["..."],
"sourceRepository": "owner/repo" | null,
"ageHours": 0
}

verdict is block whenever the engine produces any blocking reason, otherwise allow. sourceRepository is the GitHub owner/repo the version was published from (from verified provenance or the continuity baseline) when available.

Config resolution

The server resolves safeinstall.config.json exactly like the CLI. When a config file is found it is respected exactly. When none is found, the server uses a recommended secure preset — the built-in defaults with typoSquat.mode and continuity.mode promoted to block, because the agent use case wants maximum signal.

The MCP SDK (@modelcontextprotocol/sdk) is an optional dependency, loaded lazily only when safeinstall mcp runs — CLI-only users never install it. npx safeinstall-cli mcp pulls it in automatically; if it is missing the command prints an install hint and exits non-zero. Full setup: mcp/README.md.

Agent guard — enforcement, not advice

New in 0.9.0. The MCP tool is advisory — an agent can consult it. The agent guard is enforcement: SafeInstall registers as a pre-shell-execution hook for Claude Code and Cursor, so every shell command an agent wants to run is intercepted before it executes — whether or not the agent cooperates.

one-time setup, per project
$safeinstall guard install
# writes the hook into .claude/settings.json and .cursor/hooks.json
# limit to one client: safeinstall guard install --client claude

The guard never evaluates policy itself. It detects package installs — including aliases (npm i, bun a), env-var prefixes, wrappers like sudo and corepack, chained commands, and pipes — and denies them with the exact rewritten command routed through SafeInstall. Blocking becomes steering: a well-behaved agent self-corrects in one step, and the install then runs with full policy evaluation and lifecycle scripts disabled.

what the agent sees
$cd app && npm i axios && npm test
Denied: raw package install.
Run instead:
cd app && safeinstall npm install axios && npm test

Decision model

CommandVerdictWhy
npm i axios · pnpm add zod · bun a xdenyRaw install — the agent gets the SafeInstall-routed equivalent to run instead.
safeinstall npm install axiosallowAlready routed through the policy engine.
npx create-thing · pnpm dlx x · bunx xaskPackage runners download and execute registry code with no install-time checks — the user decides. A locally installed binary (nearest node_modules/.bin) is recognized and allowed without a prompt.
npm install $(cat pkgs.txt) · yarn add xdenyCannot be analyzed with confidence (command substitution, variable expansion, yarn) — fail-closed on the security-relevant path.
git status · npm test · npm run buildallowNon-install commands produce no opinion — the guard never bricks the agent's shell.

The guard needs no network access and answers in milliseconds. Hook registration is idempotent and conservative: existing hooks are preserved, a malformed settings file is left untouched and reported. For Cursor the hook is registered with failClosed: true, so a crashed hook blocks instead of silently allowing.

Honest limit: the guard sees shell commands, not intent. An install performed by a program the agent writes and runs (a Node script calling npm programmatically) is not intercepted. It raises the bar for the common path; it is not a sandbox.

Agent Trust Surface — self-defending policy

New in 0.10.0. The guard stops raw installs — but a prompt-injected agent does not have to fight it. It can rewrite the rules: weaken safeinstall.config.json, delete the hook from .cursor/hooks.json, plant an invisible-Unicode instruction in CLAUDE.md, or register a malicious MCP server. Those files program the next agent session — in the agent era, they are the persistence surface.

safeinstall trust lock records a hash baseline of that surface. SafeInstall reconciles the real state against it before every guard decision and every install/check, so tampering is detected — even a bypass the guard never sees (a node -e install, or deleting the hook to silence the guard).

lock the surface and scaffold the CI anchor
$safeinstall trust lock --ci github
Trust surface locked (4 file(s), mode: warn).
CI: wrote .github/workflows/safeinstall-trust.yml (pins safeinstall-cli@0.10.0).

The three zones

ZoneFilesOn drift
Enforcementsafeinstall.config.json, .claude/settings.json, .cursor/hooks.json, the trust workflow, .safeinstall/Every agent command is denied until a human approves.
InstructionAGENTS.md, CLAUDE.md, .cursorrules, .cursor/rules/**, .github/copilot-instructions.mdHidden Unicode (Trojan Source, incl. U+061C) always blocks; content drift warns, or blocks in strict mode.
Tool.mcp.json, .cursor/mcp.json, MCP blocks in .claude/settings.jsonMCP changes deny installs and runners; unpinned (rug-pull) servers are flagged.

Instruction content drift only warns by default because Claude Code writes to CLAUDE.md itself (its memory feature); hidden Unicode has no legitimate purpose in these files and always blocks.

Commands

$safeinstall trust lock [--mode warn|strict] [--ci github]
$safeinstall trust status [--require-lock]
$safeinstall trust approve
$safeinstall trust unlock

trust status is read-only and exits 2 on drift — use it in CI. trust approve and trust unlock read confirmation from the controlling terminal and refuse to run in CI or agent-hook contexts, so an agent cannot re-baseline or remove the surface on its own.

CI is the durable anchor

trust lock --ci github scaffolds .github/workflows/safeinstall-trust.yml, which re-verifies the committed baseline on every pull request with the CLI pinned to an exact version. It runs on a machine the agent does not control, so tampering done on a compromised local machine does not survive review. The workflow file is itself part of the tracked surface — disabling it is drift.

Two things a CLI cannot do for you: make the workflow a required status check in branch protection, and require review of .safeinstall/ and .github/workflows/ (e.g. via CODEOWNERS). The automatic check catches inconsistent tampering; a fully consistent rewrite of the whole baseline is only caught by a human reviewing the diff.

Honest scope

Locally this is tamper-evident, not tamper-proof, and it is user-space: a scheme-aware agent in your own account can bypass interception, rewrite the in-repo lock and ledger into a consistent state, and delete the local head mirror — user space cannot stop that. What it cannot subvert is CI re-verification of the committed lock on a machine it does not control. Rely on the CI check for a real adversary; the local layer catches mistakes and non-targeted tampering.

Exceptions

allowedPackages — exact name match skips release-age, install-script, and typo-squat checks (with a warning); source, trust-downgrade, and provenance checks still apply. allowedScripts — per-package lifecycle phases you accept for policy; SafeInstall may still forward ignore-scripts by default via packageManagerDefaults.

{
"allowedPackages": ["internal-tooling"],
"allowedScripts": {
"esbuild": ["postinstall"]
}
}

Limitations

  • Not a CVE scanner — pair with npm audit or other tools for vulnerability data.
  • Transitive dependencies are evaluated for install scripts and untrusted sources when transitive.mode is enabled. Release-age, typo-squat, and provenance still apply to direct dependencies only.
  • Transitive install-script detection is npm-only — pnpm lockfiles do not record install-script presence. Transitive untrusted-source detection works for both npm and pnpm.
  • Trust script comparison needs the previous resolved tree under node_modules.
  • Bun project installs are not fully lockfile-aware.
  • peerDependencies are not evaluated unless also declared as direct dependencies.
  • Ambiguous metadata leads to a block, not a silent allow.
  • Typo-squat target list is curated and refreshed manually between releases; brand-new packages published in the last day may not yet appear on the list.
  • Provenance verification supports GitHub Actions trusted publishers on the public Sigstore root only. GitLab CI, self-hosted Sigstore, and other trust roots are currently out of scope.
  • Provenance continuity catches attestation downgrades and source-repository changes between versions; it cannot catch an attack delivered through a legitimately-compromised CI workflow that still produces valid provenance from the real repository (the Shai-Hulud worm class). There is no identity discontinuity to detect there.
  • MCP tools are advisory. The check_package tool is available once the server is configured, but the agent only consults it if instructed to by a rule snippet. For enforcement at the shell layer, use the agent guard.
  • The agent guard intercepts shell commands in Claude Code and Cursor. It does not see installs performed by programs the agent writes and runs, and a human at a plain terminal is not gated. It raises the bar for the common agent path; it is not a sandbox.
  • Git sources are identified by URL for allowlist and trusted-publisher purposes, not by inferred package name. This is intentional: conflating registry axios with github:any-fork/axios would be dangerous.

Out of scope

SafeInstall is a pre-install guard, not a full platform.

×Vulnerability databases / CVE scanning
×Registry proxy or tarball rewriting
×Hosted dashboards or cloud policy services
×Malware detection or package content analysis (pair with a content analyzer like Socket.dev or Phylum if you need that)
×GitLab CI / Bitbucket Pipelines trusted publishers (GitHub Actions only)
×Self-hosted Sigstore instances (public Sigstore root only)
×Transitive release-age, typo-squat, and provenance checks (direct dependencies only by design)
×Selective lifecycle script execution

License

SafeInstall is open source, released under the MIT license. Source code is on GitHub. Free forever for individual use. Team features (shared policy, org-wide enforcement) are a planned commercial addition — not part of the current CLI.

Provided as-is under MIT. Not a security guarantee. SafeInstall is a policy tool. It does not guarantee the safety of any package, does not detect all supply-chain attacks, and does not replace professional security review.