npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

ghostimport

v0.6.0

Published

Stops AI coding agents from installing npm packages that don't exist. MCP server + hooks that block slopsquatting and typosquat supply-chain attacks.

Readme


Your agent invents a package name. It doesn't exist on npm yet. Someone is watching for exactly that, and the moment they register it, npm install runs their postinstall script on your machine.

This is slopsquatting. Most dependency scanners inspect packages after they are installed; ghostimport checks names before they are — against the live registry, at the moment your agent writes or installs them.

$ ghostimport

  Scanned 142 files · 38 packages

  ✗ react-server-fetch  does not exist on npm
    ↳ src/data/loader.ts
    ↳ unregistered — anyone could claim this name with a malicious postinstall

  ✗ axois  high risk
    ↳ src/api/client.ts
    ↳ 1-2 chars from 'axios' — likely a typo
    ↳ has postinstall script — runs code on npm install
    created 2019-08-29 · 1245/week · 1 version

  2 problems found.

Install

npm install -g ghostimport     # or: npx ghostimport

Node.js 22+. Zero runtime dependencies — the published package uses only Node built-ins.

Use it with your AI agent

This is the part that matters. A CI check tells you about a bad package after it's in your repo; these stop it at the moment it's written.

Hooks — the enforcing one

Add to .claude/settings.json:

{
  "hooks": {
    "PreToolUse": [
      { "matcher": "Bash", "hooks": [{ "type": "command", "command": "ghostimport hook" }] }
    ],
    "PostToolUse": [
      { "matcher": "Edit|Write|MultiEdit", "hooks": [{ "type": "command", "command": "ghostimport hook" }] }
    ]
  }
}
  • PreToolUse on Bash — reads any install command and denies the tool call if it would fetch a package that doesn't exist, is a typosquat, or ships an install script. This is the one that stops a real attack.
  • PostToolUse on edits — checks imports the agent just wrote, or dependencies it just added to a package.json, and tells it to fix them.

The agent sees why it was stopped:

ghostimport blocked this: the install command below would fetch packages that
are unsafe or do not exist.

  • 'axois' exists but is high risk: name is 1-2 chars from 'axios';
    single version published.

Do not retry this command as written.

It fails open. Registry unreachable, malformed payload, or a check exceeding its 20-second budget → exits 0 and stays out of the way. A security tool that wedges your agent when you're offline gets uninstalled by Friday.

MCP — the self-service one

Lets the model verify a name before it writes the import. Hooks are mandatory; MCP tools are offered — use both.

claude mcp add ghostimport -- npx -y ghostimport mcp

Add to .cursor/mcp.json or your client's config:

{
  "mcpServers": {
    "ghostimport": {
      "command": "npx",
      "args": ["-y", "ghostimport", "mcp"]
    }
  }
}

| Tool | What it does | |---|---| | check_packages | Verify package names exist on npm. deep adds risk heuristics. | | check_install_command | Vet a full npm/pnpm/yarn/bun install command. | | scan_project | Audit a whole directory. |

CLI

ghostimport              # scan the current directory
ghostimport ./src        # scan a folder
ghostimport --json       # machine-readable
ghostimport --watch      # re-scan on change

Exits 1 if any imported package doesn't exist, so it works as a CI gate as-is.

| Flag | Effect | |---|---| | --quiet, -q | Only show problems | | --json | Output results as JSON | | --watch, -w | Re-scan on file changes | | --badge | Print a README badge after scanning | | --fast | Skip the deep supply-chain check on undeclared packages | | --no-undeclared | Hide "imported but not in package.json" warnings | | --no-cache | Bypass the 24h registry cache | | --version, -v · --help, -h | |

Optional .ghostimportrc.json in your project root:

{ "ignore": ["@company/*", "internal-lib"], "includeUndeclared": true }
- uses: FGuerreir0/[email protected]
  with:
    path: '.'

Or just run: npx ghostimport --quiet.

For pre-commit, in .pre-commit-config.yaml:

repos:
  - repo: https://github.com/FGuerreir0/ghostimport
    rev: v0.6.0
    hooks:
      - id: ghostimport

API

import { verifyPackages, scan } from 'ghostimport'

await verifyPackages(['axios', 'axois'], { deep: true })
// [ { pkg: 'axios', status: 'ok', typosquatOf: null },
//   { pkg: 'axois', status: 'suspicious', risk: 'high', typosquatOf: 'axios', ... } ]

const { missing, undeclared, risks } = await scan('./src')

status is 'ok' | 'missing' | 'suspicious' | 'unknown'. 'unknown' means the registry was unreachable — never treat it as a failure.

| Export | Purpose | |---|---| | scan(dir, opts?) | Scan a directory. Returns ScanResult. | | verifyPackages(names, opts?) | Check a list of names. Returns PackageVerdict[]. | | checkNpm(name) | Does this one package exist? | | checkPackageRisk(name) | Full supply-chain check for one package. | | detectTyposquat(name) | Returns the popular package it's 1-2 chars from, or null. | | extractImports(code) | Package names from a source string. | | extractInstallTargets(cmd) | Packages a shell command would install. |

interface ScanResult {
  scanned: number
  packages: number
  missing: { pkg: string; files: string[]; claimable: boolean | null }[]
                                                  // don't exist on npm; claimable is false
                                                  // inside a scope someone already owns
  undeclared: { pkg: string; files: string[] }[]  // exist, but not in package.json
  risks: RiskEntry[]                              // supply-chain findings
  errors: { pkg: string; error: string; files: string[] }[]
  cacheHits: number
}

type RiskEntry =
  | { pkg: string; files: string[]; type: 'unregistered'; typosquatOf: string | null }
  | { pkg: string; files: string[]; type: 'suspicious'
      risk: 'medium' | 'high'; flags: string[]; installScripts: string[]
      typosquatOf: string | null; maintainers: number
      created: string; downloads: number | null; versions: number }

Types are shipped with the package: ScanResult, ScanOptions, MissingRef, RiskEntry, PackageVerdict, VerdictStatus, PackageRiskResult, NpmCheckResult, Config.

Detects: import, require(), dynamic import(), export … from, scoped packages, subpath imports (pkg/utils → pkg), and <script> blocks in .vue, .svelte and .astro (markup is ignored, so a package name in template text is never flagged). Also every registry dependency declared in any package.json, whether or not anything imports it — workspace:, file:, link:, git and URL specs are skipped, and npm: aliases are followed to the real name.

Extensions: .js .jsx .ts .tsx .mjs .cjs .vue .svelte .astro

Ignores: Node built-ins, relative imports, path aliases (@/, ~/, $lib/, tsconfig paths), URL/protocol imports, virtual modules, workspace packages, and node_modules/ dist/ build/ .git/.

Risk signals:

| Signal | Weight | Why | |---|---|---| | postinstall / preinstall / install script | critical | Runs arbitrary code on npm install | | Name 1-2 chars from a popular package | critical | Classic typosquat | | Created < 30 days ago | medium | No track record | | < 50 weekly downloads | medium | Near-zero adoption | | Single version published | medium | Abandoned or one-shot | | Single maintainer | amplifier | Only counts alongside another signal |

high if any critical signal fires, or 2+ medium ones. Only medium and high are reported.

A name that doesn't exist on npm is reported as squattable unless it sits in a scope someone already owns — only a scope's owner can publish under it, so @babel/made-up is a broken import but not a squatting target. That check costs at most one request per scope, so --fast doesn't disable it.

Contributing

Development setup, the source layout, and how the site in docs/ is built and deployed all live in CONTRIBUTING.md.

Support

ghostimport is free, MIT licensed and has no runtime dependencies — and it stays that way. If it caught something for you, you can buy me a coffee.

License

MIT