# Package Safety Gate (npm, PyPI, crates.io) (`jetlaunch/package-safety-gate`) Actor

Pre-install check for npm, PyPI and crates.io packages. Catches hallucinated and typosquatted names, flags install scripts, advisories and young packages, and returns allow, warn or block. Decision support only.

- **URL**: https://apify.com/jetlaunch/package-safety-gate.md
- **Developed by:** [Chris O'Byrne](https://apify.com/jetlaunch) (community)
- **Categories:** Developer tools, Agents, AI
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $5.00 / 1,000 package checks

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

### What does Package Safety Gate do?

Package Safety Gate is a pre-install check for **npm, PyPI and crates.io** packages. Call it before an AI coding agent or a developer runs `npm install`, `pip install` or `cargo add`. It looks up each package in its public registry and other open data sources, then returns a verdict (`allow`, `warn` or `block`) with a plain list of reasons.

It is built to catch the problems that agents and busy developers hit most often:

- **Hallucinated package names.** A name that does not exist in the registry is reported as `exists: false` with a `block` verdict. Attackers register such names ("slopsquatting"), so installing a made-up name is risky.
- **Typosquats.** Names that sit one or two edits away from a popular package (transposed letters, look-alike characters, dropped separators, scope tricks) are listed with the popular package they resemble.
- **Risky packages.** Brand-new packages, very low download counts, install-time scripts (npm), known advisories and malicious-package records from OSV, deprecated or yanked versions, and low OpenSSF Scorecard scores.

**A verdict is decision support only.** It is a heuristic summary of public data. It is not a security audit, it does not read package code, and it cannot promise that a package is safe or unsafe. Read the reasons and make the call.

### Who is it for?

- Teams that run AI coding agents and want a gate before dependencies are added.
- Developers who want a quick sanity check on an unfamiliar dependency.
- CI jobs that review a `package.json`, `requirements.txt` or `Cargo.toml` change.

### How to use it

1. Choose a mode: **package-check** or **lockfile-check**.
2. For package-check, list packages as `{"ecosystem": "npm", "name": "express"}` objects. A `version` is optional and can be an exact version, a range or a tag. The short form `npm:express@4.18.2` also works.
3. For lockfile-check, paste up to 50 dependencies as `name@version` lines, or paste a `package.json`, `requirements.txt` or `Cargo.toml`.
4. Run the Actor and read the dataset (table view "Verdicts"). The key-value store record `OUTPUT` holds a run summary with counts and the worst verdict.

You can also call it from code or from an AI agent through the Apify API or the Apify MCP server. Example with the API (add your own token):

```
curl -X POST "https://api.apify.com/v2/acts/jetlaunch~package-safety-gate/run-sync-get-dataset-items?token=YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"mode":"package-check","packages":[{"ecosystem":"npm","name":"express"}]}'
```

### Input

| Field | Meaning |
| --- | --- |
| `mode` | `package-check` or `lockfile-check` |
| `packages` | package-check only. Up to 100 entries of `{ecosystem, name, version?}` or `"npm:name@version"` strings. Ecosystems: `npm`, `pypi`, `crates` |
| `lockfile` | lockfile-check only. Up to 50 dependencies, pasted as text |
| `lockfileFormat` | `auto`, `lines`, `package.json`, `requirements.txt`, `Cargo.toml` |
| `ecosystem` | Needed for plain lines and requirements.txt text. package.json is npm and Cargo.toml is crates.io |

The default input checks `express` (npm), `requests` (PyPI) and a name that does not exist, so you can see an `allow` and a `block` verdict in one run.

### Output

One dataset item per package. A shortened example for a hallucinated name:

```json
{
  "ecosystem": "npm",
  "name": "zq-hallucinated-pkg-91827",
  "status": "ok",
  "verdict": "block",
  "exists": false,
  "reasons": [
    {
      "code": "not-found",
      "severity": "block",
      "message": "Package \"zq-hallucinated-pkg-91827\" does not exist on the npm registry. A name that does not exist is a common sign of a hallucinated dependency, and registering it would let an attacker supply the code."
    }
  ],
  "typosquat": { "isPopularPackage": false, "neighbours": [] },
  "disclaimer": "Decision support only, not a security guarantee."
}
```

For a package that exists you also get: `resolvedVersion`, `ageDays`, `firstPublishedAt`, `latestVersion`, `latestVersionDate`, `weeklyDownloads` (with its basis), `maintainerCount`, `installScripts` (npm: which of preinstall, install and postinstall are present), `advisories` (count, highest severity, malware flag, OSV ids and links), `scorecard` (OpenSSF Scorecard score when a GitHub repository is linked), `repository`, `license`, and `typosquat` (whether the package is itself popular, and the closest popular names with the kind of match).

Reasons carry a severity: `block`, `warn` or `info`. The verdict is the highest severity present.

The output contains no free text written by package authors (no descriptions, no script bodies), so it is safe to hand to an AI agent without prompt-injection risk from package metadata.

### Pricing

This Actor uses pay-per-event pricing. You pay only for the events below, plus nothing for failed lookups.

| Event | Price | When it is charged |
| --- | --- | --- |
| Package check | $0.005 | Once per package that returns a verdict, including a verdict for a name that does not exist |
| Lockfile check | $0.03 | Once per lockfile check of up to 50 dependencies, when at least 80% of the registry lookups succeed |

Not charged: entries with an invalid name or ecosystem, lookups where the registry could not be reached, and lockfile runs where fewer than 80% of lookups succeeded. If you set a maximum cost per run, the Actor stops cleanly when the limit is reached.

### How verdicts are decided

| Signal | Effect |
| --- | --- |
| Package or exact version not found, or unpublished | block |
| OSV malicious-package record for the version, or npm security placeholder | block |
| Very close name to a popular package, little adoption (under 1,000 weekly downloads), and young (under 180 days) or no linked repository | block |
| Close name to a popular package | warn (info if the package is well adopted) |
| First published under 30 days ago, under 100 weekly downloads | warn |
| npm install scripts (info for packages with over 1 million weekly downloads) | warn |
| Known advisories with HIGH or CRITICAL severity for the checked version | warn (other severities: info) |
| Deprecated or yanked version | warn |
| OpenSSF Scorecard under 3 | warn (info for packages with over 1 million weekly downloads) |
| OSV could not be reached | warn |
| No repository link, one maintainer on a young package, stale for 3 years, very new version | info |

### Data sources and licences

The Actor uses official public APIs only and paces its requests. Each request carries a User-Agent that names this Actor and a contact address.

- npm registry and the npm downloads API (api.npmjs.org). Registry requests are limited to 1 per second per run, in line with npm's crawler policy.
- PyPI JSON API and pypistats.org for download counts (the pypistats etiquette asks clients to avoid bulk crawling; this Actor looks up only the packages you submit).
- crates.io: the sparse index first, then the API only for downloads, repository and owner counts, with at most 1 API request per second and an identifying User-Agent, as the crates.io data access policy requires.
- OSV (api.osv.dev). Each advisory keeps the licence of its source database.
- deps.dev API (Google). Generated data is available under CC-BY 4.0. Use is subject to the Google API Terms of Service.
- OpenSSF Scorecard API (api.scorecard.dev). Data is licensed under CDLA Permissive 2.0.
- Popular-name lists for typosquat checks are bundled: the top 3,000 npm names (from the MIT-licensed npm-high-impact package), the top 3,000 PyPI names (from the monthly hugovk/top-pypi-packages ranking) and the top 1,500 crates by downloads (from the crates.io API). The list date is stored in every run summary.

Package Safety Gate is not affiliated with or endorsed by npm, PyPI, the Python Software Foundation, crates.io, the Rust Project, Google, OSV, deps.dev or the OpenSSF. Product names are used only to describe what is checked.

### FAQ

**Does it scan package code?** No. It uses registry metadata, advisories, popularity and name similarity. It reports whether an npm version declares install scripts but does not read them.

**Why does a popular package sometimes get a warning?** Install scripts, deprecated versions or known advisories can apply to any package. Read the reasons.

**Why is `maintainerCount` empty for some PyPI packages?** PyPI reports owners only for the project, and organization-owned projects show no individual owners. The field is left empty rather than guessed.

**How are crates.io weekly downloads calculated?** crates.io publishes a 90-day total. The weekly figure is that total divided by 90 and multiplied by 7, and the output says so.

**How are version ranges handled?** npm and crates.io ranges are resolved against the published versions. For PyPI only exact pins (`==`) are resolved; other specifiers are checked against the latest version, and the output says how the version was chosen.

**How long does a run take?** Requests are paced to respect the upstream limits. In platform test runs a 50-dependency list took about 55 seconds for npm, about 77 seconds for PyPI and about 110 seconds for crates.io. A single package takes a few seconds.

### Limitations

- Only the public registries listed above. Private registries, git, URL and path dependencies are reported as not checkable.
- The typosquat check compares against bundled popular-name lists, not the whole registry.
- Scorecard results exist only for GitHub repositories that Scorecard has analyzed.
- A new malicious package that no advisory database knows yet can still receive `allow`. That is why the verdict is decision support only.

### Support

Open an issue in the Issues tab of this Actor. Include the input you used and the run ID.

# Actor input Schema

## `mode` (type: `string`):

package-check looks at each package in the list below. lockfile-check reads a pasted dependency list of up to 50 entries and returns one verdict per dependency plus an overall verdict.

## `packages` (type: `array`):

Up to 100 packages. Each entry is an object with ecosystem (npm, pypi or crates), name, and an optional version (exact version, range or tag). A short form also works: "npm:express@4.18.2", "pypi:requests", "crates:serde". The prefilled third name does not exist on purpose, to show the block verdict.

## `lockfile` (type: `string`):

Up to 50 dependencies. Paste name@version lines (one per line), the contents of a package.json, a requirements.txt, or a Cargo.toml. Only used when mode is lockfile-check.

## `lockfileFormat` (type: `string`):

Auto detects package.json (starts with a curly brace) and Cargo.toml (has a \[dependencies] table). Anything else is read as one dependency per line.

## `ecosystem` (type: `string`):

Needed only for plain name@version lines and requirements.txt text. package.json is always npm and Cargo.toml is always crates.io.

## Actor input object example

```json
{
  "mode": "package-check",
  "packages": [
    {
      "ecosystem": "npm",
      "name": "express"
    },
    {
      "ecosystem": "pypi",
      "name": "requests"
    },
    {
      "ecosystem": "npm",
      "name": "zq-hallucinated-pkg-91827"
    }
  ],
  "lockfile": "lodash@4.17.20\nexpress@4.18.2\nreact@^18.2.0",
  "lockfileFormat": "auto",
  "ecosystem": "npm"
}
```

# Actor output Schema

## `results` (type: `string`):

No description

## `summary` (type: `string`):

No description

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "packages": [
        {
            "ecosystem": "npm",
            "name": "express"
        },
        {
            "ecosystem": "pypi",
            "name": "requests"
        },
        {
            "ecosystem": "npm",
            "name": "zq-hallucinated-pkg-91827"
        }
    ],
    "lockfile": `lodash@4.17.20
express@4.18.2
react@^18.2.0`
};

// Run the Actor and wait for it to finish
const run = await client.actor("jetlaunch/package-safety-gate").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {
    "packages": [
        {
            "ecosystem": "npm",
            "name": "express",
        },
        {
            "ecosystem": "pypi",
            "name": "requests",
        },
        {
            "ecosystem": "npm",
            "name": "zq-hallucinated-pkg-91827",
        },
    ],
    "lockfile": """lodash@4.17.20
express@4.18.2
react@^18.2.0""",
}

# Run the Actor and wait for it to finish
run = client.actor("jetlaunch/package-safety-gate").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "packages": [
    {
      "ecosystem": "npm",
      "name": "express"
    },
    {
      "ecosystem": "pypi",
      "name": "requests"
    },
    {
      "ecosystem": "npm",
      "name": "zq-hallucinated-pkg-91827"
    }
  ],
  "lockfile": "lodash@4.17.20\\nexpress@4.18.2\\nreact@^18.2.0"
}' |
apify call jetlaunch/package-safety-gate --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,jetlaunch/package-safety-gate"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/YZ264dZ6dCN9eKAcu/builds/830f64Mdv73q4mCNZ/openapi.json
