pompelmi/pompelmi
Minimal Node.js wrapper around ClamAV — scan any file and get Clean, Malicious, or ScanError. Handles installation and database updates automatically.
No tracked packages depend on this.
pompelmi Pro — audit logs, priority support, and enterprise features. Learn more
# 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
| 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 |
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:
clamscan as a child process and maps its exit code to a verdict. No stdout parsing, no regex.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.
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.
npx pompelmi scanjustsouichi/pompelmi-scanner on Docker Hub: ClamAV + HTTP scan API in one pull (docs)npx pompelmi scorecard (docs)packages/vscode/) (docs)watch ./uploads --quarantine ./quarantine auto-moves infected files with sidecar JSON--report (docs)--share-card (docs)scan(filePath, [options]) function — works locally or against a remote clamd instancescanBuffer(buffer, [options]) — scan in-memory Buffers directly, no temp file required in TCP modescanStream(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 arraysscanS3(params, [options]) — scan S3 objects by streaming directly from AWS S3, no disk I/OcreatePool([options]) — persistent connection pool for high-throughput clamd scanningwatch(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 dependenciescreateScanner([options]) — EventEmitter-based scanner; call .scan(filePath) or .scanDirectory(dirPath) and listen to 'clean', 'malicious', 'scanError', and 'error' eventscreateCache([options]) — skip rescanning known-clean files; LRU eviction, configurable TTL, optional file-backed persistence; zero extra dependencies (docs)createPolicy(rules) — unified size, MIME type, extension, and virus rules in one object; Express middleware and NestJS guard included (docs)createMultiEngine(options) — combine ClamAV and VirusTotal with any/all/majority consensus; per-engine verdict breakdown; zero extra dependencies (docs)scanDirectory.stream(dirPath) — async-iterable progress events (progress / result / complete) for real-time UI feedbackretries and retryDelay options on every scan functionVerdict.Clean / Verdict.Malicious / Verdict.ScanError) — typo-proof comparisonshost/port) or UNIX socket (socket) with configurable timeoutimport { scan } from 'pompelmi' works out of the box (dual CJS/ESM build)import { scan } from 'npm:pompelmi' — no install step required@pompelmi/cloudflare — Web APIs only, no Node.js built-insSee how pompelmi compares to other Node.js ClamAV 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 |
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);
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);
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 }))
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 })
}
// +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 }
}
}
Bun.file() for faster file readingnpm:pompelmi — no install step required@pompelmi/cloudflare — connects to a remote clamd over TCPpompelmi does not bundle or automatically download ClamAV. Install it once per machine (see Installing ClamAV).
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
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.
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.');
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);
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 });
});
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;
}
}
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)));
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));
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.');
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.');
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.
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.
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 |
# macOS
brew install clamav && freshclam
# Linux (Debian / Ubuntu)
sudo apt-get install -y clamav clamav-daemon && sudo freshclam
# Windows (Chocolatey)
choco install clamav -y
The examples/ directory contains standalone runnable scripts and framework-specific 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 |
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 |
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.
- uses: actions/checkout@v4
- name: Virus scan
uses: pompelmi/pompelmi@v1.7.0
- uses: actions/checkout@v4
- name: Virus scan
id: scan
uses: pompelmi/pompelmi@v1.7.0
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 }}"
| 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 |
| 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.
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
git checkout -b feat/your-change.npm test passes.main.Please read CODE_OF_CONDUCT.md before contributing. To report a security vulnerability, see SECURITY.md.
@pompelmi/cloudflare ships in v1.17.0PompelmiModule.forRoot() with injectable PompelmiServicepompelmi is free and open source. If your company uses pompelmi in production, consider sponsoring development:
What sponsorship funds:
ISC — © pompelmi contributors
pompelmi.app · npm · GitHub