ghostimport Docs Releases GitHub npm

Documentation

Set up ghostimport

Everything the tool does, on one page. If you read only one section, make it agent hooks — that's the part that actually blocks an attack.

$ npm install -g ghostimport

Install

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

install
# global — recommended if you'll use the hook
npm install -g ghostimport

# one-off, no install
npx ghostimport

# as a dev dependency
npm install --save-dev ghostimport

If you're wiring up the hook, install globally. Hooks run on every matching tool call, and npx pays a startup cost each time.

Quick start

Point it at a project. One mode, no flags to discover — the supply-chain analysis always runs.

ghostimport
$ 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
    ↳ single version published
    created 2019-08-29 · 1245/week · 1 version

  ⚠ not in package.json: zod

  2 problems found.

It exits 1 when an imported or declared package doesn't exist and 0 otherwise, so it works as a CI gate with no extra configuration.

Agent hooks

The enforcing integration. Hooks run on every matching tool call whether or not the model cooperates — unlike MCP tools, which the model has to choose to call.

Add to .claude/settings.json:

.claude/settings.json
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [{ "type": "command", "command": "ghostimport hook" }]
      }
    ],
    "PostToolUse": [
      {
        "matcher": "Edit|Write|MultiEdit",
        "hooks": [{ "type": "command", "command": "ghostimport hook" }]
      }
    ]
  }
}

What each one does

  • PreToolUse on Bash — parses any npm, pnpm, yarn or bun 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, because it fires before the postinstall could run.
  • PostToolUse on edits — reads the file the agent just wrote, extracts its imports, and reports any that don't resolve. For a package.json, it checks the dependencies the edit added instead.

What the agent sees when it's blocked

stderr
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. Remove the flagged packages, correct
the name if it was a typo, and tell the user what was flagged and why.

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

Results are cached for 24 hours in ~/.ghostimport/, so the steady state is zero network calls.

MCP server

Gives the model tools to check a name before it commits to writing the import. Use it alongside hooks, not instead of them — this one depends on the model choosing to call it.

Claude Code
claude mcp add ghostimport -- npx -y ghostimport mcp

For Cursor, Windsurf, Zed or any other MCP client, add to its config:

.cursor/mcp.json
{
  "mcpServers": {
    "ghostimport": {
      "command": "npx",
      "args": ["-y", "ghostimport", "mcp"]
    }
  }
}

Tools it exposes

ToolArgumentsWhat it does
check_packages packages, deep Verify names exist on npm. deep adds the risk heuristics.
check_install_command command Extract and vet every package a shell command would install.
scan_project path Audit a whole directory.

The server speaks JSON-RPC over stdio and is written directly against the wire format — no MCP SDK — so installing ghostimport still pulls in zero runtime dependencies. A supply-chain tool with its own dependency tree undercuts its own argument.

CLI

usage
ghostimport [dir] [options]
ghostimport mcp | hook

Options

FlagEffect
--quiet, -qOnly show problems
--jsonOutput results as JSON
--watch, -wRe-scan on file changes
--badgePrint a README badge after scanning
--fastSkip the deep supply-chain check on undeclared packages
--no-undeclaredHide "imported but not in package.json" warnings
--no-cacheBypass the 24-hour registry cache
--version, -vPrint the version
--help, -hShow help

--fast only skips the deep check on packages that exist. Names that don't exist are always reported as squattable — that check costs no extra requests, so there's no way to accidentally turn off the important part.

Commands

  • ghostimport mcp — run as an MCP server over stdio
  • ghostimport hook — run as an agent hook, reading a hook payload on stdin

JSON output

ghostimport --json
{
  "scanned": 142,
  "packages": 38,
  "missing":    [{ "pkg": "react-server-fetch", "files": [...], "claimable": true }],
  "undeclared": [{ "pkg": "zod", "files": [...] }],
  "risks":      [{ "pkg": "axois", "type": "suspicious", "risk": "high", ... }],
  "errors":     [],
  "cacheHits":  33
}

README badge

Run a scan with --badge and it prints ready-to-paste Markdown for your project's README, reflecting the result of that scan.

ghostimport --badge
$ ghostimport --badge

  README badge:
  [![ghostimport](https://img.shields.io/badge/ghostimport-%E2%9C%93%20clean-brightgreen)](https://github.com/FGuerreir0/ghostimport)

The two states

ghostimport: clean Clean. Every imported package resolves on npm.
ghostimport: 2 ghosts Ghosts found. The count is how many imported packages don't exist.

Copy the clean badge

[![ghostimport](https://img.shields.io/badge/ghostimport-%E2%9C%93%20clean-brightgreen)](https://github.com/FGuerreir0/ghostimport)

The badge is static. It captures the scan you ran when you generated it — paste it and it stays that way, even if a later commit introduces a package that doesn't exist. Treat it as a signal that you check imports, not as live status. A self-updating badge is planned for a future release; until then, pair it with the CI check, which is what actually fails the build.

Config file

Optional. Drop a .ghostimportrc.json in your project root.

.ghostimportrc.json
{
  "ignore": ["@company/*", "internal-lib"],
  "includeUndeclared": true
}
FieldDefaultDescription
ignore[]Names or scope patterns to skip. @scope/* is supported.
includeUndeclaredtrueWarn on packages that exist but aren't in package.json.

Workspace packages in a monorepo and paths aliases from your tsconfig are detected automatically — you don't need to list them.

JavaScript API

Ships with TypeScript declarations. Seven exports, each with one job.

node
import { verifyPackages, scan } from 'ghostimport'

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

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

Verdict statuses

okExists on npm, no risk signals fired.
missingDoes not exist. Anyone can register the name — unless it sits in a scope someone already owns, which claimable: false marks.
suspiciousExists, but the heuristics rate it medium or high risk.
unknownCouldn't be checked — network or timeout. Never treat this as a failure.

Exports

ExportPurpose
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)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.

What gets scanned

Detected: import, require(), dynamic import(), export … from, scoped packages, and subpath imports (pkg/utils resolves to pkg).

Declared: every registry dependency 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

For .vue, .svelte and .astro, only <script> blocks and Astro frontmatter are read — markup is ignored, so a package name that appears in template text is never flagged.

Ignored:

  • Node built-ins, including node: prefixed ones
  • Relative imports (./, ../)
  • Path aliases — @/, ~/, $lib/, and tsconfig paths
  • URL and protocol imports, and virtual modules (virtual:, Vite internals)
  • Workspace packages in a monorepo
  • node_modules/, dist/, build/, .git/

Risk signals

A name that resolves isn't automatically safe. These signals are checked against packages that exist but aren't declared in your package.json.

SignalWeightWhy it matters
postinstall / preinstall / install scriptCriticalRuns arbitrary code the moment the package lands
1–2 chars from a popular nameCriticalThe classic typosquat shape
Created < 30 days agoMediumNo track record to judge it by
< 50 weekly downloadsMediumNobody else is depending on it
Single version publishedMediumAbandoned, or published once for a reason
Single maintainerAmplifierOnly counts alongside another signal — plenty of good packages have one

Rated high if any critical signal fires or two medium ones do; only medium and high are reported.

CI

The CLI already exits non-zero on a missing package, so the simplest form is one line.

.github/workflows/ci.yml
- name: Check for hallucinated packages
  run: npx ghostimport --quiet

Or use the action, which also writes a job summary and exposes counts as outputs:

.github/workflows/ci.yml
- uses: FGuerreir0/ghostimport@v0.5.0
  with:
    path: '.'

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

.pre-commit-config.yaml
repos:
  - repo: https://github.com/FGuerreir0/ghostimport
    rev: v0.5.0
    hooks:
      - id: ghostimport

CI is a backstop, not the main event. By the time a bad package reaches CI it's already in your repo — hooks catch it while it's being written.