Prerequisites

Requirement Minimum version Notes
Node.js 18.x 18+ required (uses built-in node:test runner). Test with node --version.
npm 6.x Comes with Node.js.
ClamAV 0.103+ Must be installed on the host system. See Step 1.

Step 1 — Install ClamAV

pompelmi delegates to the clamscan system binary. Install ClamAV for your operating system before proceeding.

macOS — Homebrew
brew install clamav
Linux — Debian / Ubuntu
sudo apt-get update
sudo apt-get install -y clamav clamav-daemon
Linux — RHEL / CentOS / Fedora
sudo dnf install -y clamav clamav-update
Windows — Chocolatey
choco install clamav -y
Windows — manual

Download the installer from clamav.net/downloads and follow the setup wizard. Make sure the ClamAV bin directory is added to PATH.

Verify the installation:

clamscan --version
On macOS, Homebrew installs clamscan to /opt/homebrew/bin (Apple Silicon) or /usr/local/bin (Intel). Both are on the default PATH after a standard Homebrew setup.

Step 2 — Update virus definitions

ClamAV ships with no virus definitions. You must download them before the first scan. This is done with freshclam.

macOS and Windows
freshclam
Linux
sudo freshclam
On Linux, the clamav-freshclam service may be running in the background and will lock the database files. Stop it before running freshclam manually:
sudo systemctl stop clamav-freshclam
sudo freshclam
sudo systemctl start clamav-freshclam

freshclam downloads the main.cvd, daily.cvd, and bytecode.cvd definition files. The download is several hundred megabytes on first run. Subsequent runs are incremental.

Schedule freshclam to run daily (cron, systemd timer, or Task Scheduler) to keep definitions current. Stale definitions reduce detection quality.

Step 3 — Install pompelmi

npm install pompelmi

Or with yarn:

yarn add pompelmi

Verify it installed:

node -e "const p = require('pompelmi'); console.log(typeof p.scan)"
# function

Step 4 — Run your first scan

Create a file called scan.js:

const pompelmi = require('pompelmi');

pompelmi.scan(process.argv[2])
  .then(result => {
    console.log('Result:', result);
    process.exit(result === 'Malicious' ? 1 : 0);
  })
  .catch(err => {
    console.error('Error:', err.message);
    process.exit(2);
  });

Run it against any file:

node scan.js /etc/hosts
# Result: Clean
To test the malicious detection path, ClamAV ships with a built-in test signature called the EICAR test file. It is a harmless string that every AV engine detects as malware for testing purposes:
echo 'X5O!P%@AP[4\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*' > /tmp/eicar.txt
node scan.js /tmp/eicar.txt
# Result: Malicious

Step 5 — Handle errors properly

scan() rejects the Promise in three situations. Always attach a .catch() or use try/catch in an async function.

const pompelmi = require('pompelmi');
const path = require('path');

async function safeScan(filePath) {
  // Resolve to absolute path to avoid ambiguity
  const abs = path.resolve(filePath);

  let result;
  try {
    result = await pompelmi.scan(abs);
  } catch (err) {
    if (err.message.startsWith('File not found')) {
      console.error('The file does not exist:', abs);
      return null;
    }
    if (err.message.startsWith('Unexpected exit code')) {
      console.error('ClamAV returned an unknown exit code.');
      return null;
    }
    // clamscan binary not found or other OS error
    console.error('ClamAV error:', err.message);
    return null;
  }

  return result;
}

safeScan('./upload.zip').then(console.log);

More examples

Scanning uploaded files in Express

const express = require('express');
const multer  = require('multer');
const pompelmi = require('pompelmi');
const path = require('path');
const fs   = require('fs');

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

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

  try {
    const result = await pompelmi.scan(filePath);

    if (result === 'Malicious') {
      fs.unlinkSync(filePath);
      return res.status(400).json({ error: 'Malware detected. Upload rejected.' });
    }

    if (result === 'ScanError') {
      // Scan failed — treat as untrusted, reject upload
      fs.unlinkSync(filePath);
      return res.status(422).json({ error: 'Scan incomplete. Upload rejected as precaution.' });
    }

    // Clean — move to permanent storage
    const dest = path.join('/var/app/uploads', req.file.filename);
    fs.renameSync(filePath, dest);
    res.json({ status: 'ok', file: req.file.filename });

  } catch (err) {
    fs.unlinkSync(filePath);
    res.status(500).json({ error: 'Scan failed: ' + err.message });
  }
});

app.listen(3000);

Scanning multiple files in sequence

const pompelmi = require('pompelmi');
const fs = require('fs');

async function scanDirectory(dirPath) {
  const files = fs.readdirSync(dirPath)
    .map(f => `${dirPath}/${f}`)
    .filter(f => fs.statSync(f).isFile());

  const results = [];

  for (const file of files) {
    const result = await pompelmi.scan(file);
    results.push({ file, result });
    if (result !== 'Clean') {
      console.warn(`[${result}] ${file}`);
    }
  }

  return results;
}

scanDirectory('./downloads').then(results => {
  const malicious = results.filter(r => r.result === 'Malicious');
  console.log(`Scanned ${results.length} files. ${malicious.length} malicious.`);
});

Parallel scanning with Promise.all

const pompelmi = require('pompelmi');

async function scanAll(filePaths) {
  const settled = await Promise.allSettled(
    filePaths.map(f => pompelmi.scan(f).then(r => ({ file: f, result: r })))
  );

  return settled.map(s =>
    s.status === 'fulfilled'
      ? s.value
      : { file: '?', result: 'Error', error: s.reason.message }
  );
}

scanAll(['/tmp/a.zip', '/tmp/b.pdf', '/tmp/c.exe'])
  .then(results => console.table(results));
Parallel scanning spawns one clamscan process per file simultaneously. On systems with limited RAM, scanning tens of files in parallel may cause memory pressure because each process loads ClamAV definitions independently. Prefer sequential scanning for large batches.

Common errors

Error message Cause Fix
File not found: /path/to/file The path passed to scan() does not exist on disk. Check the path. Use path.resolve() to avoid relative-path bugs.
ENOENT clamscan is not in PATH. Install ClamAV (Step 1) and ensure it is on the system PATH.
Unexpected exit code: N ClamAV returned an exit code other than 0, 1, or 2. Run clamscan --version to check if ClamAV is healthy. Exit code 50+ usually indicates a config or permission error.
Scan always returns ScanError ClamAV definitions are missing, corrupted, or the process lacks read permissions. Run freshclam to download/repair definitions. Check file permissions.
LibClamAV Error: cli_loaddbdir() Virus database not found by ClamAV. Run freshclam to populate the database directory.
Permission denied on the file The process does not have read access to the file. Check file permissions. pompelmi runs clamscan as the current user.