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).
Installation
- Use Node.js >=20.
- Install globally or invoke with npx — no account or registry auth required.
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.
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.
Blocked: release too newRegistry packages must be at least 72 hours old by default (minimumReleaseAgeHours).
Blocked: install script presentpreinstall, install, and postinstall on the resolved registry version are blocked unless allowed per package via allowedScripts.
Blocked: untrusted sourceallowedSources defaults to registry, workspace, file, and directory. Git, tarball, and URL installs are outside the default set.
Blocked: trust level droppedBlocks 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).
Blocked: suspected typo-squatRequested 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.
Blocked: publisher mismatch / attestation missing / attestation invalid / attestation unreachableCryptographically 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.
Blocked: transitive install script / transitive untrusted sourceWalks 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.
Blocked: provenance downgrade / identity discontinuityLearns 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.
| Field | Role |
|---|---|
| minimumReleaseAgeHours | Minimum age in hours for registry versions. |
| registryUrl | npm-compatible registry URL for metadata (mirrors, Artifactory, Verdaccio). |
| allowedScripts | Package name → allowed script names. Does not remove ignore-scripts unless you change packageManagerDefaults. |
| allowedSources | Source types permitted (add git, tarball, url only if you intend to allow them). |
| allowedPackages | Names 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. |
| packageManagerDefaults | Per-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.minNameLength | Minimum length of a requested package name to participate in the typo-squat check. Names shorter than this are skipped. Defaults to 4. |
| typoSquat.ignore | Array 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.requireFor | Array of package name patterns (exact or glob with * wildcard). Provenance is required for these packages even when mode is "warn". |
| provenance.trustedPublishers | Object 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.checks | Array 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.baselineSize | Number 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.
safeinstall check
Evaluates direct dependencies only. On success, stderr includes: Check passed: no direct dependency policy violations found.
safeinstall init
Writes a starter safeinstall.config.json in the current directory. Refuses if the file exists unless you pass --force.
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.
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
By default the action runs safeinstall check --json against direct dependencies. The job fails if any dependency is blocked by policy.
Install mode
In install mode the action runs the package manager through SafeInstall so policy is enforced before packages are written to disk.
Inputs
| Input | Default | Description |
|---|---|---|
| mode | check | "check" audits deps; "install" runs the PM through SafeInstall. |
| package-manager | pnpm | npm, pnpm, or bun (install mode only). |
| args | — | Additional arguments forwarded to the package manager. |
| config-path | — | Explicit path to safeinstall.config.json. |
| version | latest | SafeInstall CLI version to install. |
Outputs
| Output | Description |
|---|---|
| decision | allow or block. |
| summary | Human-readable one-line summary. |
| exit-code | 0 (allow), 2 (block), 1 (error). |
| json | Full 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.
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:
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.
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.
Decision model
| Command | Verdict | Why |
|---|---|---|
| npm i axios · pnpm add zod · bun a x | deny | Raw install — the agent gets the SafeInstall-routed equivalent to run instead. |
| safeinstall npm install axios | allow | Already routed through the policy engine. |
| npx create-thing · pnpm dlx x · bunx x | ask | Package 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 x | deny | Cannot be analyzed with confidence (command substitution, variable expansion, yarn) — fail-closed on the security-relevant path. |
| git status · npm test · npm run build | allow | Non-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).
The three zones
| Zone | Files | On drift |
|---|---|---|
| Enforcement | safeinstall.config.json, .claude/settings.json, .cursor/hooks.json, the trust workflow, .safeinstall/ | Every agent command is denied until a human approves. |
| Instruction | AGENTS.md, CLAUDE.md, .cursorrules, .cursor/rules/**, .github/copilot-instructions.md | Hidden 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.json | MCP 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
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.
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.
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.