SafeInstall
safeinstall-cli · 0.14.0

Your AI agent installs packages. SafeInstall decides which ones run.

The open, local-first install guard for Claude Code, Codex, and Cursor. Typosquats, hours-old malware releases, and provenance breaks are blocked before npm, pnpm, or bun ever executes — and the policy defends itself against the agent, too.

One command: starter config + guard hooks + tamper-evident baseline

$npx safeinstall-cli init

Global install

$npm install -g safeinstall-cli

Node >=20 · Open source · MIT license

Only 3 runtime dependencies · Sigstore and MCP load on demand

Claude Code — raw install rewritten in place
# the agent wants to run:
$npm install axios
# the guard hook rewrites it in place —
# the permission prompt shows the rewritten command:
Allow: safeinstall npm install axios ?
Allowed: policy checks passed.
blocked — policy output (exit 2)
$safeinstall pnpm add compromised-pkg@9.9.9
Using config: ./safeinstall.config.json
Install blocked.
- compromised-pkg@9.9.9
Blocked: release too new (…; minimum is 72 hours).
Blocked: install script present (… has postinstall).
Three agents, one guard

Hooked at the shell layer — whether or not the agent cooperates.

The guard intercepts every shell command before it executes. Each client gets the strongest behavior its hook protocol supports — no protocol is stretched past what it can actually enforce.

Claude Code
PreToolUse/Bash · .claude/settings.json
Raw installs

Rewritten in place: the hook returns the SafeInstall-routed command, and Claude's permission dialog shows the rewritten command — you approve safeinstall npm install axios, never the raw install. Verified end-to-end against Claude Code v2.1.206.

Registry runners (npx …)

Ask — Claude's own approval prompt puts the decision in front of you before registry code is downloaded and executed.

Codex
PreToolUse/Bash · .codex/hooks.json
Raw installs

Rewritten in place via updatedInput, same as Claude Code — the agent's install runs through SafeInstall without a round-trip.

Registry runners (npx …)

Fail closed. Codex hooks support no ask decision, so runners are denied outright rather than silently allowed.

One-time step: review and trust the project hook with Codex /hooks — Codex skips unreviewed hooks by design.

Cursor
beforeShellExecution · .cursor/hooks.json
Raw installs

Denied with the exact SafeInstall-routed command to run instead — a well-behaved agent self-corrects in one step.

Registry runners (npx …)

Ask — Cursor prompts you before registry code runs. The hook is registered with failClosed: true, so a crashed hook blocks instead of silently allowing.

one command registers all of it
$npx safeinstall-cli init
Info: Config: created safeinstall.config.json.
Info: Claude Code: hook registered in new file .claude/settings.json.
Info: Codex: hook registered in new file .codex/hooks.json.
Info: Cursor: hook registered in new file .cursor/hooks.json.
Info: Trust surface locked (4 file(s), 0 MCP server(s), mode: warn).
Init complete — config: created, guard: claude, codex, cursor, trust surface: locked.

safeinstall init registers hooks for the clients actually present in the project (detected via .claude/, CLAUDE.md, .codex/, AGENTS.md, .cursor/, .cursorrules) — or pass --client claude,codex,cursor explicitly.

Why this exists

Package install is now an attack surface.

Supply-chain attacks through npm and similar registries are increasing. The way developers install packages has changed. The tooling has not kept up.

01

Package installs are an attack surface.

Lifecycle scripts run arbitrary code at install time. A single malicious dependency can exfiltrate tokens, modify files, or establish persistence — before your app even starts.

02

AI coding makes this worse.

When an AI assistant suggests a package, most developers install it immediately. There is no pause for research, no audit, no second opinion. The speed that makes AI coding powerful also removes the friction that used to catch bad packages.

03

Scanners alert after the fact.

npm audit and most security tools tell you about known vulnerabilities in packages you already installed. They do nothing about install-time script execution. They do not prevent the package from running during install.

04

The fix is not more dashboards.

Developers need a guardrail that blocks many risky installs before they run—policy first, then the package manager. Not only a report after the fact.

05

Valid signatures are not enough.

A compromised npm maintainer can publish a malicious version that cryptographically verifies — signed by a GitHub Actions workflow the attacker controls in a fork of the real repository. Scanners and signature-only checks accept it. Install-time policy with trusted publisher pinning refuses it.

What it blocks

Eight protections. Four on by default, four opt-in.

SafeInstall evaluates policy before your package manager runs. Four checks run by default. Typo-squat detection, Sigstore provenance verification, transitive dependency evaluation, and provenance continuity are opt-in per project via safeinstall.config.json.

Blocked: release too new

Fresh registry releases

By default, registry versions newer than 72 hours are blocked (configurable via minimumReleaseAgeHours). That window is where many supply-chain publishes get noticed late.

Blocked: install script present

Lifecycle scripts

Packages with preinstall, install, or postinstall scripts are blocked unless you allow those scripts per package in allowedScripts. SafeInstall still forwards --ignore-scripts to the package manager by default so installs stay non-blind.

Blocked: untrusted source

Non-registry sources

Git, tarball, and direct URL installs are blocked unless that source type is in allowedSources. Registry, workspace, file, and directory sources are allowed by default.

Blocked: trust level dropped

Trust downgrades

Two cases: a registry dependency moves to git/url/tarball, or a new registry version introduces lifecycle scripts where the installed version had none (compared using local node_modules when present).

Blocked: suspected typo-squat

Typo-squat detection

Requested package names are compared against a curated list of popular npm packages via Damerau-Levenshtein distance with transposition. Close-but-not-exact matches like raect, lodsh, and axois are flagged. Opt-in via typoSquat.mode = 'warn' or 'block'. Minimum name length and per-project ignore list are configurable.

Blocked: publisher mismatch

Provenance verification

Fetches the Sigstore attestation bundle for registry packages, verifies signatures against the public Sigstore trust root, checks Rekor transparency log inclusion, and extracts the source repository from the SLSA v1 provenance statement. Pin the expected owner/repo per package via trustedPublishers. Mismatches always block, even in warn mode, because a valid signature from the wrong source is what maintainer-account compromise looks like.

Blocked: transitive install script / untrusted source

Transitive dependencies

Walks the full lockfile tree and flags transitive packages that declare lifecycle scripts or resolve from untrusted sources. Zero extra registry calls — reads directly from pnpm-lock.yaml or package-lock.json. The ua-parser-js attack class: a deeply nested dependency running code at install time. Opt-in via transitive.mode = 'warn' or 'block'.

Blocked: provenance downgrade / identity discontinuity

Provenance continuity

Learns a per-package trust baseline from the provenance identity of recent versions, then blocks deviations: a version that arrives with no attestation where recent ones had it (downgrade), or one attested from a different source repository (identity discontinuity). The fingerprint of an account-compromise publish. Packages that never adopted provenance simply have no baseline, so the check stays silent — no false positives. Reads npm's attestation metadata, so it works without the optional sigstore package. Opt-in via continuity.mode = 'warn' or 'block'.

The unique property

Catching a maintainer-compromise attack.

A valid Sigstore signature is not enough. An attacker who compromises an npm maintainer account can publish a malicious version of a package you already trust, and the attestation on that malicious version will cryptographically verify — signed by a GitHub Actions workflow the attacker controls in a fork of the real repository. SafeInstall catches this. Pin the expected source repository with provenance.trustedPublishers and any build that comes from anywhere else is blocked, even if the signature is valid.

publisher mismatch — blocked
$safeinstall check
Using config: ./safeinstall.config.json
Check blocked.
- axios@1.99.0
Blocked: publisher mismatch for axios (expected axios/axios, got evil-org/axios).
Suggestion: Verify the package source. Update provenance.trustedPublishers only if the change is intentional.

This is the only install-time policy gate that enforces the source-repository property of an npm provenance attestation. CVE scanners look for known vulnerabilities. Content analyzers look for suspicious code. SafeInstall verifies that the cryptographic chain of trust points at the repository you agreed to trust — and refuses anything else.

Pinning is the strongest form, but it needs you to declare the expected repository up front. Provenance continuity (new in 0.7.0) closes the gap for everything you have not pinned: it learns a per-package baseline from the provenance identity of recent versions and blocks the deviations npm itself does not — a version that suddenly arrives with no attestation (the fingerprint of a publish from a stolen token) or one attested from a different repositorythan the established baseline. It reads npm's published attestation metadata, so it works without the optional sigstore package. Packages that never adopted provenance have no baseline and the check stays silent — no false positives, no global “require provenance” sledgehammer. Opt in with continuity.mode.

provenance verified — dogfood
$safeinstall check
Using config: ./safeinstall.config.json
Info: safeinstall-cli: provenance verified from Mickdownunder/SafeInstall via .github/workflows/release.yml.
Check passed: no direct dependency policy violations found.

SafeInstall verifies its own Sigstore attestation against its own GitHub repository via the public Sigstore transparency log.

How it works

Evaluate first. Execute second.

No registry proxy. No middleware on downloads. Policy runs locally, then the underlying tool runs as usual.

1

You run SafeInstall

Prefix your package manager command with safeinstall.

$safeinstall pnpm install
2

SafeInstall evaluates

For registry packages it uses public npm registry metadata (publish time, scripts). Project installs for pnpm/npm use the lockfile so versions match what the repo will install.

3

Block or allow

Policy violations exit with code 2 and print Blocked: … lines. Clean runs print Allowed: policy checks passed. then invoke the manager.

4

Package manager runs

On allow, your exact command runs. SafeInstall appends defaults like --ignore-scripts per packageManagerDefaults unless you change them.

stderr — allow
$safeinstall pnpm add zod
Using config: built-in defaults
Allowed: policy checks passed.
stderr — untrusted source
$safeinstall npm install git+https://github.com/foo/bar
Install blocked.
- git+https://github.com/foo/bar
Blocked: untrusted source (git).
vs. scanners

Scanners report. SafeInstall runs first.

npm audit and similar tools report on known issues in dependencies you already installed. SafeInstall evaluates policy before the package manager: it can refuse installs that violate age, script, or source rules, and it forwards --ignore-scripts by default unless you change defaults — see docs for behavior.

Scanners (npm audit, Snyk…)SafeInstall
When it runsAfter installBefore install
What it checksKnown CVEs in installed packagesInstall policy: age, scripts, source
OutputVulnerability reportAllow or block, with Blocked: … lines
Lifecycle scriptsDoes not block installBlocks if unallowed; default forwards --ignore-scripts to PM
Fresh release windowNoYes (default 72h minimum age)
Registry metadataVariesUses npm registry API when evaluating registry packages
Requires security expertiseTo act on results: yesReadable block reasons
Maintainer-compromise detectionNoYes (via trusted publisher pinning)
Provenance verificationNoYes (via Sigstore public trust root)
Typo-squat detectionNoYes (embedded top-N list, Damerau-Levenshtein)
Transitive dependency checksLimited (post-install only)Yes (lockfile tree walk, zero registry calls)
GitHub Action for CINo (manual setup)Yes (Mickdownunder/SafeInstall@v1)

Use both. They solve different problems. SafeInstall is pre-install; scanners are post-install.

Built for AI coding

Vibe coding is fast.
Blind installs are the cost.
SafeInstall is the gate.

AI assistants suggest packages in seconds. They don't check publish dates. They don't read install scripts. They don't verify the source. You type “yes” and move on.

SafeInstall adds one layer between suggestion and execution: policy. Release age, lifecycle scripts, source type, trust signals — checked before the package manager runs. You keep the speed. You lose the blind spot.

If you vibe code, this is the safety net your workflow is missing.

The MCP server (0.8.0) lets the agent consult the policy engine — point Claude Code, Codex, Cursor, Windsurf, or Cline at it and add the agent rule, and check_package is called before any install is suggested.

Enforcement (since 0.9.0). The guard hooks Claude Code, Codex, and Cursor at the shell layer. Every command the agent runs is intercepted before execution: raw installs are rewritten in place through SafeInstall (Claude Code and Codex) or denied with the exact routed command (Cursor), and package runners like npx require your approval — whether or not the agent cooperates.

Self-defending (since 0.10.0). The guard stops installs — the Agent Trust Surface stops the agent from rewriting the guard. It locks the files that configure SafeInstall and your agent, and re-verifies them in CI.

Works with every AI tool that suggests or runs installs

CursorGitHub CopilotClineClaude CodeWindsurfAiderDevinContinueCodex

Typical vibe coding workflow

You"add a PDF generation library"
AIpnpm add pdf-maker@0.1.2 — "try this"
Beforepnpm add pdf-maker@0.1.2
Nowsafeinstall pnpm add pdf-maker@0.1.2
SafeInstallInstall blocked. Blocked: release too new.

Enforce it — one command, per project

agent guard
$npx safeinstall-cli init
# agent runs: npm i axios
→ rewritten in place:
safeinstall npm install axios

Registers pre-execution shell hooks for Claude Code, Codex, and Cursor — and locks the trust surface over them. Raw installs come back routed through SafeInstall, so the agent self-corrects in one step. Guard docs → · MCP setup →

Agent Trust Surface · hardened through 0.14.x

Your agent's config files are the new crontab.

A prompt-injected agent doesn't have to fight the guard — it can just rewrite the rules: weaken safeinstall.config.json, delete the hook, plant an invisible instruction in CLAUDE.md, or register a malicious MCP server. Those files program the next session. In the agent era, they are where persistence lives.

safeinstall trust lock records a hash baseline of that surface. SafeInstall reconciles the real state against it before every guard decision and every install — so tampering shows up as drift, even a bypass the guard never sees.

And it knows the difference between git and tampering (0.12.0): the out-of-workspace ledger mirror reconciles by hash-chain containment, so a pull or rebase that brings reviewed, CI-verified trust entries fast-forwards silently — while rewrites and rollbacks of trust history still hard-block. Your normal workflow stays quiet; the alarms are reserved for tampering.

No SafeInstall tool at the market does this. Every other guard asks “is this package bad?” The trust surface asks the question the agent era added: did anyone quietly change the rules?

Lock it, and wire the CI anchor

one command, per project
$safeinstall trust lock --ci github
Trust surface locked (4 files, mode: warn).
CI: wrote .github/workflows/safeinstall-trust.yml
# re-verifies the baseline on every pull request

The generated workflow re-hashes the committed baseline on a machine the agent does not control — the durable anchor. Trust Surface docs →

Three zones, proportionate responses

Enforcement
safeinstall.config.json, agent hook configs (.claude, .codex, .cursor), the CI workflow, .safeinstall/
Any change locks down every agent command until a human approves.
Instruction
AGENTS.md, CLAUDE.md, .cursorrules, .cursor/rules, copilot-instructions.md
Hidden Unicode (Trojan Source, incl. U+061C) always blocks; content drift warns.
Tool
.mcp.json, .cursor/mcp.json, MCP blocks in agent settings
MCP changes block installs; unpinned (rug-pull) servers are flagged.

Honest scope. Locally this is tamper-evident against mistakes and non-targeted tampering, not tamper-proof against a scheme-aware agent in your own account. The durable guarantee is CI re-verifying the committed lock — which is exactly why trust lock --ci scaffolds it for you.

Proof, not promises

A security tool should meet its own standard.

Every claim on this page is backed by something you can check in the repository — an advisory, a workflow file, a test corpus, a diff you can run yourself.

GHSA-xrmc-c5cg-rv7x

We published a security advisory against our own guard.

Property-based fuzzing of our own shell parser found command forms the guard could miss. We filed the advisory ourselves, fixed and disclosed within 24 hours, and every captured bypass class now lives on as a permanent adversarial regression corpus in the repository — run on every test suite, alongside an independent reference detector.

Reproducible releases

Byte-reproducible, with Sigstore provenance.

Every release is published from CI via trusted-publisher OIDC with a Sigstore provenance attestation — cryptographically traceable to the tagged commit and workflow run. The tarball is byte-reproducible from that tag: run npm pack against the tagged commit and diff it against the registry yourself.

External verifier

The thing that verifies trust is its own locked-down repo.

Trust verification runs from safeinstall-verifier — a separate, code-owner-locked public repository. It installs the CLI pinned by exact version and sha512 content hash and reads a candidate checkout strictly as data. A CI-required adversarial suite makes every tampered case fail and the clean fixture pass, so a vacuous verifier can never go green. Since 0.13, this repo invokes it by full commit SHA — pinned, not floating.

Decision records

Every install decision leaves a record you can re-check.

In a git repository, each policy-evaluated install writes an actor-tagged, explicitly untrusted record into .safeinstall/decisions/ — bound by git blob OID to the manifest, lockfile, config, and trust lock, before and after the package manager ran. safeinstall decisions verify re-checks that a base..head delta is covered by schema-valid, linkage-continuous chains anchored at both committed ends. It verifies integrity and completeness; it never trusts a recorded verdict.

Attack Lab

A catalogue of attacks against us that cannot lie about itself.

attack-lab/ is a machine-readable catalogue: every case carries its reproducible starting state, the expected SafeInstall layer, and a named regression test in the normal suite. The eval maps each to a status — an honest limit stays a known-gap and cannot be silently relabelled defended, and an unpatched bypass marked public fails the run as a discipline error. Run it yourself: node attack-lab/run.mjs.

The gate that said no

trust approve has already refused a real coding agent.

Approving trust-surface changes requires a human at the controlling terminal (/dev/tty) — never stdin — and refuses CI, Claude Code, Cursor, and Codex sessions. The Codex refusal is not theoretical: the CODEX_SHELL gate shipped in 0.11.1 because a Codex session on the developer's own terminal was in a position to approve its own trust-surface changes. Now it cannot.

CLI examples

Same commands — SafeInstall in front.

Start with safeinstall init (config + guard hooks + trust baseline in one run). Also supported: safeinstall --json … and project installs that resolve direct deps from lockfiles (pnpm / npm).

pnpm add — allowed
$safeinstall pnpm add axios
Using config: built-in defaults
Allowed: policy checks passed.
Release too new (hours threshold)
$safeinstall npm install react@latest
Install blocked.
- react@…
Blocked: release too new (…; minimum is 72 hours).
Suggestion: Retry later or lower minimumReleaseAgeHours…
Git / URL source blocked
$safeinstall bun add github:owner/repo
Install blocked.
- …
Blocked: untrusted source (git).
safeinstall check — direct deps
$safeinstall check
Using config: ./safeinstall.config.json
Check passed: no direct dependency policy violations found.
Typo-squat caught
$safeinstall pnpm add raect
Using config: ./safeinstall.config.json
Install blocked.
- raect
Blocked: Suspected typo-squat: "raect" is 1 edit(s) away from popular package "react".
Suggestion: Verify you meant to install "react". If this package is intentional, add "raect" to typoSquat.ignore.
Transitive install script caught
$safeinstall check
Using config: ./safeinstall.config.json
Check blocked.
- 1 transitive dependency declares an install script: sketchy@0.0.1.
Suggestion: Review these packages. Install scripts run arbitrary code at install time.
Provenance verified
$safeinstall check
Using config: ./safeinstall.config.json
Info: safeinstall-cli: provenance verified from Mickdownunder/SafeInstall via .github/workflows/release.yml.
Check passed: no direct dependency policy violations found.
CI / teams

Five lines of YAML. Policy on every pull request.

SafeInstall ships as a reusable GitHub Action. Add it to any workflow — no CLI installation, no configuration required. Defaults apply immediately. Blocked dependencies appear in the GitHub Actions job summary with the exact block reason.

.github/workflows/safeinstall.yml
# .github/workflows/safeinstall.yml
name: SafeInstall Policy Check
on: [pull_request]
jobs:
policy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
- uses: Mickdownunder/SafeInstall@v1

Supports check mode (audit direct dependencies without installing) and install mode (enforce policy during the actual install step). Pin a specific CLI version for reproducible builds, or let it track latest.

Check mode
Audit deps on every PR.
Install mode
Enforce policy during pnpm install.
Job summary
Block reasons visible without clicking into logs.
Outputs
decision, summary, exit-code, json for downstream steps.

And the trust-surface backstop scaffolds itself.

safeinstall trust lock --ci github writes a second workflow that re-verifies the committed Agent Trust Surface baseline on every pull request — on a machine the agent does not control.

Since 0.12.0 the scaffolded workflow is content-hash-pinned: it installs the verifier CLI from a tarball whose sha512 was recorded from the registry at scaffold time (trust on first use). A registry that later serves different bytes for the same version fails the check — the verifier cannot be silently swapped. Any registry error at scaffold time fails closed; a weaker, version-only workflow is never written.

Since 0.13 the verification logic itself lives in a separate, code-owner-locked repository — safeinstall-verifier — guarded by a CI-required adversarial suite, and this repo invokes it by full commit SHA.

scaffold the CI anchor
$safeinstall trust lock --ci github
Trust surface locked (4 file(s), mode: warn).
CI: wrote .github/workflows/safeinstall-trust.yml
# verifier pinned by sha512 — not just by version
Who it is for

Built for developers who ship fast.

A CLI guardrail, not a platform. Scope is intentional: pre-install policy only.

Solo developers

Your machine, your responsibility.

  • AI-assisted workflows mean more installs and less package review
  • No security team to catch risky publishes for you
  • One global install — policy runs locally, no account needed
  • Use safeinstall check on direct dependencies before you ship
Startup engineering teams

Move fast. Don't run installs blind.

  • Small teams ship quickly; one bad install can hit everyone
  • Commit safeinstall.config.json so policy matches across machines
  • Exit code 2 on block is CI-friendly when you wrap installs in pipelines
  • pnpm and npm project installs honor lockfiles — fewer surprise versions
  • Add the GitHub Action to your CI pipeline — policy runs on every pull request without developers needing to remember a CLI prefix
  • Enable transitive.mode to catch risky install scripts buried deep in the dependency tree — the attack surface npm audit misses
AI-first teams

Your AI writes the install command. You control the gate.

  • Using Claude Code, Codex, Cursor, Copilot, Cline, or Windsurf
  • AI suggestions skip the usual review instincts — SafeInstall does not
  • Same prefix (safeinstall) for npm, pnpm, and bun
  • JSON mode (--json) fits automation around agents and scripts
  • GitHub Action catches risky installs even when the AI commits directly to a branch without human review
Get started

Free forever. Open source. Protected in one command.

One run writes the config, hooks your agents, and locks the trust surface. No account, no sign-up, no registry auth. Read the docs · View source

$npx safeinstall-cli init
$npm install -g safeinstall-cli
GitHub Actionuses: Mickdownunder/SafeInstall@v1

Add to any GitHub Actions workflow · Node >=20 · MIT license