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

pompelmi

v1.20.0

Published

ClamAV for humans — scan any file and get back Clean, Malicious, or ScanError. No daemons. No cloud. No native bindings.

Downloads

65,674

Readme


Quick Start

# Scan a file
npx pompelmi scan ./uploads/file.pdf

# Scan a directory
npx pompelmi scan ./uploads --recursive

# Output as JSON
npx pompelmi scan ./uploads --json

Documentation

| Guide | Description | |-------|-------------| | Getting Started | Installation, prerequisites, quickstart examples | | API Reference | Full function signatures, options, verdicts, error conditions | | CLI Reference | Terminal commands, options, examples | | S3 Integration | Scan S3 objects directly, IAM setup, Lambda pattern | | Docker / Remote Scanning | TCP sidecar, UNIX socket mount, docker-compose patterns | | GitHub Action | CI scanning, inputs/outputs, caching, example workflows |


Overview

pompelmi is a minimal Node.js wrapper around ClamAV that exposes a single async function — scan() — and returns one of three typed verdict Symbols: Verdict.Clean, Verdict.Malicious, or Verdict.ScanError. Full documentation at pompelmi.app.

It supports two scanning modes:

  • Local — spawns clamscan as a child process and maps its exit code to a verdict. No stdout parsing, no regex.
  • Remote / Docker / UNIX socket — streams the file to a running clamd daemon over TCP or a UNIX domain socket using the ClamAV INSTREAM protocol.

No cloud. No daemon required for local mode. No native bindings. Zero runtime dependencies.


Why pompelmi

If you need to scan file uploads for viruses in Node.js, integrate ClamAV with Express or Fastify, or add antivirus scanning to any upload pipeline, pompelmi is the simplest path.

Most integrations require parsing ClamAV's stdout with regex, managing a clamd daemon, or working around unmaintained packages. pompelmi does none of that: one function call, exit-code-mapped verdicts, zero dependencies.


Features

  • Standalone CLI — scan files from any terminal with npx pompelmi scan
  • Official Docker imagejustsouichi/pompelmi-scanner on Docker Hub: ClamAV + HTTP scan API in one pull (docs)
  • Security scorecard — grade your upload security A–F with npx pompelmi scorecard (docs)
  • VS Code extension — scan files directly from the IDE (scaffold in packages/vscode/) (docs)
  • Quarantine modewatch ./uploads --quarantine ./quarantine auto-moves infected files with sidecar JSON
  • HTML security dashboard — generate beautiful scan reports with --report (docs)
  • SVG share card — shareable scan result card with --share-card (docs)
  • GitHub App — one-click installation for organizations, zero-config PR scanning (docs)
  • Single scan(filePath, [options]) function — works locally or against a remote clamd instance
  • scanBuffer(buffer, [options]) — scan in-memory Buffers directly, no temp file required in TCP mode
  • scanStream(stream, [options]) — scan a Readable stream directly. In TCP mode, streamed to clamd with no disk I/O.
  • scanDirectory(dirPath, [options]) — recursively scan every file in a directory, returns clean/malicious/errors arrays
  • scanS3(params, [options]) — scan S3 objects by streaming directly from AWS S3, no disk I/O
  • createPool([options]) — persistent connection pool for high-throughput clamd scanning
  • watch(dirPath, [options], callbacks) — watch a directory and auto-scan new/modified files (300 ms debounce)
  • notify(webhookUrl, scanResult, [options]) — send a POST webhook notification when a virus is detected; optional HMAC-SHA256 signing via X-Pompelmi-Signature; zero extra dependencies
  • createScanner([options]) — EventEmitter-based scanner; call .scan(filePath) or .scanDirectory(dirPath) and listen to 'clean', 'malicious', 'scanError', and 'error' events
  • SHA256 scan cachecreateCache([options]) — skip rescanning known-clean files; LRU eviction, configurable TTL, optional file-backed persistence; zero extra dependencies (docs)
  • Scan policiescreatePolicy(rules) — unified size, MIME type, extension, and virus rules in one object; Express middleware and NestJS guard included (docs)
  • Multi-engine scanningcreateMultiEngine(options) — combine ClamAV and VirusTotal with any/all/majority consensus; per-engine verdict breakdown; zero extra dependencies (docs)
  • Directory streamingscanDirectory.stream(dirPath) — async-iterable progress events (progress / result / complete) for real-time UI feedback
  • Auto-retry on connection error — retries and retryDelay options on every scan function
  • Symbol-based verdicts (Verdict.Clean / Verdict.Malicious / Verdict.ScanError) — typo-proof comparisons
  • Full clamd support via the INSTREAM protocol — TCP (host/port) or UNIX socket (socket) with configurable timeout
  • Built-in helpers to install ClamAV and update virus definitions programmatically
  • Native ESM supportimport { scan } from 'pompelmi' works out of the box (dual CJS/ESM build)
  • Deno supportimport { scan } from 'npm:pompelmi' — no install step required
  • Cloudflare Workers — via @pompelmi/cloudflare — Web APIs only, no Node.js built-ins
  • Works with Express, Fastify, NestJS, Hono, Remix, SvelteKit, and any other Node.js HTTP framework
  • Works with Node.js • Bun • Deno • Cloudflare Workers
  • Interactive demo at pompelmi.app/demo — try before you install
  • Zero runtime dependencies — ships nothing but source code
  • Tested with EICAR standard antivirus test files
  • CommonJS + ESM module; TypeScript type declarations available inline

See how pompelmi compares to other Node.js ClamAV integrations.


Framework Integrations

Official integration packages for popular frameworks:

| Package | Framework | Install | |---------|-----------|---------| | @pompelmi/nestjs | NestJS | npm i @pompelmi/nestjs | | @pompelmi/fastify | Fastify | npm i @pompelmi/fastify | | @pompelmi/hono | Hono | npm i @pompelmi/hono | | @pompelmi/remix | Remix | npm i @pompelmi/remix | | @pompelmi/sveltekit | SvelteKit | npm i @pompelmi/sveltekit | | @pompelmi/testing | Jest/Vitest/Node | npm i -D @pompelmi/testing | | @pompelmi/cloudflare | Cloudflare Workers | npm i @pompelmi/cloudflare |

NestJS

import { PompelmiModule, PompelmiService } from '@pompelmi/nestjs';

// app.module.ts
@Module({ imports: [PompelmiModule.forRoot({ host: 'localhost', port: 3310 })] })
export class AppModule {}

// upload.service.ts
constructor(private readonly pompelmi: PompelmiService) {}
const result = await this.pompelmi.scanBuffer(file.buffer);

Fastify

const pompelmi = require('@pompelmi/fastify');
await fastify.register(pompelmi, { host: 'localhost', port: 3310 });

// Scan manually
const result = await fastify.pompelmi.scanBuffer(buffer);

// Or use the preHandler hook
fastify.post('/upload', { preHandler: fastify.pompelmi.preHandler({ field: 'file' }) }, handler);

Hono (Node.js, Bun, Cloudflare Workers)

import { Hono } from 'hono'
import { pompelmiMiddleware } from '@pompelmi/hono'

const app = new Hono()

app.use('/upload/*', pompelmiMiddleware({
  host: 'localhost',
  port: 3310,
  onInfected: (c, filename) => c.json({ error: 'Malware detected' }, 422),
}))

app.post('/upload', async (c) => c.json({ ok: true }))

Remix

import { unstable_parseMultipartFormData, json } from '@remix-run/node'
import { pompelmiUploadHandler } from '@pompelmi/remix'

export async function action({ request }) {
  // Throws HTTP 422 automatically if malware is detected
  const formData = await unstable_parseMultipartFormData(
    request,
    pompelmiUploadHandler({ host: 'localhost', port: 3310 })
  )
  const file = formData.get('file')
  return json({ name: file.name, size: file.size, ok: true })
}

SvelteKit

// +page.server.ts
import { scanUpload } from '@pompelmi/sveltekit'
import type { Actions } from './$types'

export const actions: Actions = {
  default: async ({ request }) => {
    const formData = await request.formData()
    // Throws HTTP 422 automatically if malware is detected
    await scanUpload(formData.get('file') as File, { host: 'localhost', port: 3310 })
    return { success: true }
  }
}

Requirements

  • Node.js — any LTS release (no native addons, no C++ bindings)
  • Bun — fully supported; uses Bun.file() for faster file reading
  • Deno — import from npm:pompelmi — no install step required
  • Cloudflare Workers — via @pompelmi/cloudflare — connects to a remote clamd over TCP
  • ClamAV — must be installed on the host or reachable over TCP

pompelmi does not bundle or automatically download ClamAV. Install it once per machine (see Installing ClamAV).


Installation

See pompelmi.app for the full getting-started guide.

# npm
npm install pompelmi

# yarn
yarn add pompelmi

# pnpm
pnpm add pompelmi

# bun
bun add pompelmi

Docker

Run ClamAV as a sidecar and point pompelmi at it — no local install needed on the application host.

# docker-compose.yml
services:
  clamav:
    image: clamav/clamav:stable
    ports:
      - "3310:3310"
const result = await scan('/path/to/upload.zip', {
  host: '127.0.0.1',
  port: 3310,
});

See Docker / remote scanning for details.


Usage

Basic scan

const { scan, Verdict } = require('pompelmi');

const result = await scan('/path/to/file.pdf');

if (result === Verdict.Clean)     console.log('File is safe.');
if (result === Verdict.Malicious) throw new Error('Malware detected — file rejected.');
if (result === Verdict.ScanError) console.warn('Scan incomplete — treat file as untrusted.');

Express file upload

const express = require('express');
const multer  = require('multer');
const fs      = require('fs');
const { scan, Verdict } = require('pompelmi');

const upload = multer({ dest: './uploads' });
const app    = express();

app.post('/upload', upload.single('file'), async (req, res) => {
  const filePath = req.file.path;

  try {
    const result = await scan(filePath);

    if (result === Verdict.Malicious) {
      fs.unlinkSync(filePath);
      return res.status(422).json({ error: 'Malicious file rejected.' });
    }
    if (result === Verdict.ScanError) {
      fs.unlinkSync(filePath);
      return res.status(422).json({ error: 'Scan incomplete — file rejected as precaution.' });
    }

    return res.json({ ok: true, file: req.file.filename });
  } catch (err) {
    fs.unlink(filePath, () => {});
    return res.status(500).json({ error: `Scan failed: ${err.message}` });
  }
});

app.listen(3000);

Fastify file upload

const Fastify  = require('fastify');
const { pipeline } = require('stream/promises');
const fs       = require('fs');
const path     = require('path');
const { scan, Verdict } = require('pompelmi');

const app = Fastify({ logger: true });
app.register(require('@fastify/multipart'));

app.post('/upload', async (req, reply) => {
  const data     = await req.file();
  const filePath = path.join('./uploads', `${Date.now()}-${data.filename}`);

  await pipeline(data.file, fs.createWriteStream(filePath));

  const result = await scan(filePath);

  if (result !== Verdict.Clean) {
    fs.unlinkSync(filePath);
    return reply.code(422).send({ error: result.description });
  }

  return reply.send({ ok: true });
});

Full error handling

const { scan, Verdict } = require('pompelmi');
const path = require('path');

async function safeScan(filePath) {
  try {
    const result = await scan(path.resolve(filePath));

    if (result === Verdict.ScanError) {
      // clamscan exited with code 2 — I/O error, encrypted archive, etc.
      console.warn('Scan could not complete — rejecting file as precaution.');
      return null;
    }

    return result; // Verdict.Clean or Verdict.Malicious
  } catch (err) {
    // filePath not a string, file not found, clamscan not in PATH, etc.
    console.error('Scan failed:', err.message);
    return null;
  }
}

Scan multiple files concurrently

const { scan } = require('pompelmi');
const files    = ['/uploads/a.pdf', '/uploads/b.zip', '/uploads/c.png'];

const results = await Promise.all(files.map((f) => scan(f)));

Scan a Directory

const fs = require('fs');
const { scanDirectory } = require('pompelmi');

const results = await scanDirectory('/uploads');

console.log('Clean:', results.clean);
console.log('Malicious:', results.malicious);
console.log('Errors:', results.errors);

// Delete all malicious files
results.malicious.forEach(f => fs.unlinkSync(f));

Scan a Buffer

const { scanBuffer, Verdict } = require('pompelmi');

// Useful with multer memoryStorage or any in-memory upload
const result = await scanBuffer(req.file.buffer);

if (result === Verdict.Malicious) throw new Error('Malware detected.');
if (result === Verdict.ScanError) console.warn('Scan incomplete.');

Scan a Stream

const { scanStream, Verdict } = require('pompelmi');
const { Readable } = require('stream');

// Useful for S3 getObject, HTTP downloads, or any piped source
const stream = s3.getObject({ Bucket, Key }).createReadStream();
const result = await scanStream(stream);

if (result === Verdict.Malicious) throw new Error('Malware detected.');
if (result === Verdict.ScanError) console.warn('Scan incomplete.');

Docker / Remote Scanning

Pass host and port (or socket) to switch from the local clamscan CLI to the clamd daemon. Everything else — the returned verdicts, error types — is identical.

TCP:

const result = await scan('/path/to/file.zip', { host: '127.0.0.1', port: 3310 });

UNIX socket:

const result = await scan('/path/to/file.zip', { socket: '/run/clamav/clamd.sock' });

See docs/docker.md for Docker Compose examples, UNIX socket volume mounts, scanBuffer / scanStream in clamd mode, and connection retry patterns.


Configuration

pompelmi has no configuration file or environment variables. All options are passed directly to scan().

| Option | Type | Default | Description | |--------------|----------|---------|----------------------------------------| | socket | string | — | Path to a clamd UNIX domain socket (e.g. /run/clamav/clamd.sock). Takes precedence over host/port when set. | | host | string | — | clamd hostname. Enables TCP mode when set. | | port | number | 3310 | clamd port. | | timeout | number | 15000 | Socket idle timeout in milliseconds (clamd mode only). | | retries | number | 0 | Automatic retry attempts on connection error. | | retryDelay | number | 1000 | Milliseconds to wait between retries. |

When none of socket, host, or port is provided, pompelmi spawns clamscan --no-summary <filePath> locally.


API Reference

See docs/api.md for the full reference: function signatures, options table, verdict Symbols, error conditions, and error handling patterns.

Quick summary:

| Function | Input | Disk I/O | |----------|-------|----------| | scan(filePath, [options]) | File path on disk | None in clamd mode (streamed) | | scanBuffer(buffer, [options]) | Buffer | None (streamed) | | scanStream(stream, [options]) | Node.js Readable | None (streamed) | | scanDirectory(dirPath, [options]) | Directory path | None in clamd mode | | scanS3(params, [options]) | S3 bucket + key | None (streamed from S3) | | createPool([options]) | — | Returns a ClamdPool | | watch(dirPath, [options], callbacks) | Directory path | None in clamd mode |

All four functions accept the same options object and resolve to the same three verdict Symbols:

| Symbol | Meaning | |--------|---------| | Verdict.Clean | No threats found | | Verdict.Malicious | Known signature matched | | Verdict.ScanError | Scan could not complete — treat as untrusted |


Installing ClamAV

# macOS
brew install clamav && freshclam

# Linux (Debian / Ubuntu)
sudo apt-get install -y clamav clamav-daemon && sudo freshclam

# Windows (Chocolatey)
choco install clamav -y

Examples

The examples/ directory contains standalone runnable scripts and framework-specific starters.

Framework starters

| Directory | Description | |-----------|-------------| | examples/express/ | Full Express app with multer + pompelmi middleware | | examples/nextjs/ | Next.js API route that scans raw upload bytes | | examples/nestjs/ | NestJS guard wrapping pompelmi for route-level protection |

Standalone scripts

Each can be run with node examples/<name>.js.

| File | Description | |------|-------------| | basic-scan.js | Scan a single file and log the verdict | | scan-on-upload-express.js | Express route: scan before saving | | scan-on-upload-fastify.js | Fastify route: same pattern | | scan-with-options.js | Remote clamd with custom host, port, timeout | | handle-scan-error.js | Handle every verdict including hard rejections | | delete-on-malicious.js | Auto-delete file if malicious | | quarantine-on-malicious.js | Move infected file to a quarantine folder | | scan-multiple-files.js | Concurrent scans with Promise.all | | scan-directory.js | Recursively scan every file in a directory | | scan-buffer.js | Scan an in-memory Buffer (multer memoryStorage) | | scan-stream.js | Scan a Readable stream (S3, HTTP, pipes) | | rest-api-server.js | Minimal HTTP server exposing POST /scan | | s3-scan-before-upload.js | Scan locally, then upload to S3 only if clean | | cli-scan.js | CLI tool: scan file paths, exit non-zero on threats | | scan-with-timeout.js | Timeout patterns for local and remote scanning | | scan-pdf.js | PDF upload with extension validation | | scan-image.js | Image upload with extension validation | | scan-zip.js | ZIP archive scan (ClamAV recurses automatically) | | install-clamav.js | Programmatic ClamAV installation | | update-virus-database.js | Programmatic virus DB update | | typescript-usage.ts | TypeScript example with full type declarations |


GitHub Action

GitHub Marketplace

Scan any repository for viruses on every push or pull request — ClamAV is bundled inside a Docker container, virus definitions are auto-updated at runtime, and no external services are required.

Minimal usage

- uses: actions/checkout@v4

- name: Virus scan
  uses: pompelmi/[email protected]

Full example

- uses: actions/checkout@v4

- name: Virus scan
  id: scan
  uses: pompelmi/[email protected]
  with:
    path: 'uploads/'        # scan a subdirectory instead of the whole workspace
    fail-on-virus: 'true'   # fail the workflow step on detection (default)

- name: Print infected files
  if: always()
  run: echo "${{ steps.scan.outputs.infected-files }}"

Inputs

| Input | Description | Default | |-------|-------------|---------| | path | Directory or file to scan | . (full workspace) | | fail-on-virus | Fail the workflow step when infected files are found | true | | comment-on-pr | Post a PR comment listing infected files (requires GITHUB_TOKEN) | true |

Outputs

| Output | Description | |--------|-------------| | infected-files | Newline-separated list of infected file paths (empty when clean) | | status | "clean" or "infected" |

A ready-to-copy workflow is available at .github/workflows/action-example.yml. Full reference — inputs, outputs, layer caching, and more examples — in docs/github-action.md.

For organizations: install the pompelmi GitHub App for zero-config scanning on every PR — no workflow file needed.


Contributing

Full documentation and guides are available in the Wiki.

# 1. Clone and install dev dependencies
git clone https://github.com/pompelmi/pompelmi.git
cd pompelmi
npm install

# 2. Run the test suite
npm test

# 3. Lint
npm run lint

Tests

  • test/unit.test.js — runs with Node's built-in test runner. Mocks nativeSpawn and platform dependencies; ClamAV is not required.
  • test/scan.test.js — integration tests that spawn real clamscan against EICAR test files. Skipped automatically when clamscan is not in PATH.

Submitting changes

  1. Fork the repository.
  2. Create a feature branch: git checkout -b feat/your-change.
  3. Make your changes and confirm npm test passes.
  4. Open a pull request against main.

Please read CODE_OF_CONDUCT.md before contributing. To report a security vulnerability, see SECURITY.md.


Coming soon

  • [x] Cloudflare Workers support — @pompelmi/cloudflare ships in v1.17.0
  • [x] NestJS official module — PompelmiModule.forRoot() with injectable PompelmiService

Pompelmi Cloud is on the horizon — a hosted REST API for file scanning with zero infrastructure to manage. Drop-in HTTP endpoint, no ClamAV to maintain, no daemon to run. Join the waitlist to be notified when it launches.


License

ISC — © pompelmi contributors


pompelmi.app · npm · GitHub