What pompelmi is
pompelmi is a Node.js library that provides antivirus file scanning
by delegating to ClamAV, the open-source
antivirus engine. It exposes a single async function, scan(filePath),
which returns a Promise resolving to one of three strings:
"Clean", "Malicious", or "ScanError".
The name pompelmi is the Italian plural of pompelmo (grapefruit). It was chosen because grapefruits are bright, sharp, and slightly bitter — much like a good security check.
Why it exists
Most Node.js antivirus integrations either require running a persistent ClamAV daemon
(clamd), parse raw stdout from clamscan, or wrap a native C extension.
pompelmi takes the simplest possible path:
- Spawn
clamscanas a child process. - Read the exit code when it finishes.
- Map that code to a string constant and resolve the Promise.
No daemon. No stdout parsing. No native bindings. The result is a library that is trivial to install, audit, and reason about.
How it works
When you call pompelmi.scan('/path/to/file'), the following happens:
- The file path is checked with
fs.existsSync. If missing, the Promise rejects immediately. cross-spawnlaunchesclamscan --no-summary <filePath>as a child process.- pompelmi listens for the
closeevent on the child process. - The exit code is looked up in the
SCAN_RESULTSmap:0 → "Clean",1 → "Malicious",2 → "ScanError". - The Promise resolves with the matching string, or rejects if the code is unrecognised.
The --no-summary flag suppresses ClamAV's final statistics output,
keeping stdout clean. pompelmi never reads stdout at all; only the exit code matters.
Architecture
The data flow from a calling application to a scan result:
your code
|
| pompelmi.scan(filePath)
v
ClamAVScanner.js
|
| cross-spawn('clamscan', ['--no-summary', filePath])
v
clamscan (system binary)
|
| exit code: 0 / 1 / 2
v
SCAN_RESULTS map (config.js)
|
| "Clean" / "Malicious" / "ScanError"
v
Promise resolved → your code
The installer and updater helpers follow the same pattern: they spawn the appropriate
system package manager or freshclam and report success or failure via
the exit code.
Modules
| File | Exports | Purpose |
|---|---|---|
index.js |
pompelmi |
Public entry point. Re-exports { scan }. |
ClamAVScanner.js |
scan(filePath) |
Spawns clamscan, maps exit code, returns Promise. |
ClamAVInstaller.js |
ClamAVInstaller() |
Installs ClamAV using the platform's package manager. |
ClamAVDatabaseUpdater.js |
updateClamAVDatabase() |
Runs freshclam to refresh virus definitions. |
InstallerCommand.js |
getInstallerCommand(), getUpdaterCommand() |
Returns the correct [binary, args] pair for the current platform. |
config.js |
INSTALLER_COMMANDS, UPDATER_COMMANDS, DB_PATHS, SCAN_RESULTS |
Frozen config object. Single source of truth for all commands and result strings. |
constants.js |
PLATFORM |
Exports process.platform so it can be mocked in tests. |
Dependencies
| Package | Version | Why |
|---|---|---|
| cross-spawn | ^7.0.6 |
Node's built-in child_process.spawn has edge cases on Windows
(PATHEXT, quoting). cross-spawn handles these transparently without changing
the API.
|
There are no other runtime dependencies.
Dev dependencies
| Package | Purpose |
|---|---|
eslint, @eslint/js, globals |
Static analysis. Configured in eslint.config.mjs. |
Design decisions
Why not use the clamd daemon?
The ClamAV daemon (clamd) provides faster repeated scanning because
it keeps virus definitions in memory. However, it requires managing a background
service, a socket or TCP connection, and additional configuration. pompelmi
prioritises simplicity: spawn a process, get a result, done. For high-throughput
scanning pipelines, switching to a clamd-based approach is reasonable, but that
is outside pompelmi's scope.
Why return a string instead of a boolean or number?
A boolean (true/false) would collapse the three-state result into two,
losing the "ScanError" case which represents a distinct and actionable
condition: the scan failed and the file's safety is unknown. A number would require
callers to remember what 2 means.
A string is self-documenting and safe to log, serialize, and switch on.
Why freeze the config object?
config.js exports a frozen object. This prevents accidental mutation
of shared configuration at runtime. Because config values are looked up on every
scan, a mutation bug could silently change behaviour across all scans in a process.
Why cross-spawn instead of execa or shelljs?
cross-spawn has a minimal surface area, is widely used, and does exactly one thing.
Heavier alternatives like execa add promise wrappers and output parsing
on top of what pompelmi needs, which is just an exit code.
Limitations
-
Requires ClamAV on the host.
pompelmi does not bundle or download ClamAV. The
clamscanbinary must be inPATH. -
Virus definitions must be kept up to date manually.
ClamAV's detection quality depends on fresh definitions. Run
freshclamregularly or set up a cron job. -
Scanning speed depends on file size.
Each call spawns a new
clamscanprocess, which loads definitions from disk every time. For bulk scanning, consider batching calls or switching to the clamd socket. -
No directory scanning.
scan()accepts a single file path. ClamAV supports recursive directory scanning, but pompelmi does not currently expose that option. - No streaming support. The file must exist on disk. Scanning a stream or buffer directly is not supported.