Skip to content

Migrating from ClamAV

exav is designed as a drop-in: it uses ClamAV’s signature formats, prints clamscan’s output format, and speaks the clamd wire protocol. In most setups the switch is changing one command or one socket — your signature databases, updater, and client tooling stay exactly as they are.

The flags are the part to check first. exav implements the ones day-to-day commands use and refuses the ones it does not implement, so a command line carrying an unsupported flag fails at startup rather than scanning under settings you did not ask for. The ClamAV flag matrix has every clamscan, clamd and clamdscan flag with its exav status.

ClamAV ships three programs; exav ships one that plays all three parts. The flags on the command line pick the part:

ClamAV binary exav invocation Selected by
clamscan (local scanner) exav PATH… paths, neither --listen nor --connect
clamd (daemon) exav --listen ADDR --listen
clamdscan (client) exav --connect ADDR PATH… --connect and paths

The direction is the flag: --listen is a place exav accepts connections, --connect a place it makes one. --connect plus paths means ask the daemon — the client loads no database and sends the paths over the socket. Paths on their own mean scan here, with a database this process loads itself. --connect with no paths is neither role, and exav says so rather than idling:

Terminal window
exav --connect /run/clamav/clamd.ctl
# exav: no input; provide PATH(s), `-` (stdin), or --listen

exav never inspects the name it was invoked under, so a symlink called clamdscan pointing at the binary runs a local scan like any other exav call. Two wrapper scripts are what carry an existing command line across.

Put these earlier on PATH than ClamAV’s own (/usr/local/bin before /usr/bin on most systems), and scripts, cron jobs and CI steps that call clamscan or clamdscan keep their command lines:

/usr/local/bin/clamscan
#!/bin/sh
exec exav --sig-dir /var/lib/clamav "$@"
/usr/local/bin/clamdscan
#!/bin/sh
exec exav --connect /run/clamav/clamd.ctl "$@"

Each pins the one thing the caller’s command line does not carry, and passes everything else through:

  • --sig-dir names the directory freshclam populates. exav’s own default is /var/lib/exav, so without this a caller that relied on clamscan’s default database directory gets no signature database loaded — refusing to run. A caller that passes its own -d overrides the pinned directory; the two on one command line are not a conflict.
  • --connect has to be the address the daemon listens on — the --listen of your daemon or of the service unit. --connect HOST:PORT wraps a client for a daemon on another host.

clamscan’s exit codes mean the same thing here — 0 clean, 1 a detection, 2 the scanner failed — and there is a fourth, 3, for a file exav could not fully examine. --partial-as ok folds it back into 0, which is what ClamAV answers.

A clamscan line’s flags do not all survive: exav’s are its own, and a clamscan flag exav does not have stops the run rather than being ignored, so a wrapped command line never scans under settings nobody asked for. The flag matrix is the per-flag mapping; the ones that come up most are -i--quiet, -r → nothing (directories recurse by default), --allmatch--all-matches, and the client’s --stream / --fdpass--send-as contents / --send-as fd.

The ClamAV flag matrix covers the rest of a command line — every clamscan, clamd and clamdscan flag against exav’s. It assumes you are already invoking exav; these wrappers are what get an existing clamscan / clamdscan line there.

exav reads ClamAV’s own DB files — point it at the directory freshclam (or cvdupdate) already populates:

Terminal window
exav -d /var/lib/clamav /data # Debian/Ubuntu default DB dir

Nothing changes about updating signatures: keep running freshclam / cvd on your normal schedule. For faster startup on big signature sets, build a database once:

Terminal window
exav -d /var/lib/clamav --build-db /var/lib/clamav/exav.exavdb # after each freshclam
exav -d /var/lib/clamav/exav.exavdb /data # sub-second cold start

Same output format (PATH: Signature FOUND / PATH: OK), and the same shape of command line. The simplest swap:

Terminal window
exav -d /var/lib/clamav /data # what `clamscan -r /data` scanned

For an interactive shell, alias clamscan='exav --sig-dir /var/lib/clamav'. For scripts and cron jobs, which do not read your shell aliases, use the wrapper on PATH.

exav’s flags are its own. A clamscan flag exav does not have is refused, never ignored: the run stops rather than scanning under settings you did not ask for. So a migrated command line needs its limit flags renamed, and the rename is mechanical — each exav flag is named after the bound it sets:

clamscan spelling exav flag Bounds
--max-filesize --max-input-bytes The largest top-level input scanned
--max-scansize --max-extracted-bytes What decompression may produce across one top-level file: deep-analysis size and the summed extracted bytes
--max-recursion --max-unpack-depth Nesting depth, containers inside containers
--max-files --max-members Members visited across the whole recursive walk

Two more bounds have no clamscan counterpart at all: --max-object-bytes (the most memory a single materialized object may use) and --max-matcher-bytes (cumulative bytes fed to the matcher — a CPU/time bound, not a memory one).

Aliases were the alternative, and cost more than they save: two names for one bound is two things to document, and a command line that sets the same bound under both spellings has to be refused anyway. One spelling per bound, and a clear error naming the exav flag, is the shorter path through a migration:

Terminal window
exav --max-files 500 /data
# exav: --max-files is a clamscan flag; use --max-members, members visited
# across the whole recursive walk
# exav: the full mapping is at https://exav.org/reference/clamav-flag-matrix/
ClamAV exav
A file it couldn’t fully scan (size/ratio/recursion limit, unsupported codec, encrypted) often reports OK, exit 0 status PARTIAL under LIMITS-EXCEEDED / UNSCANNABLE / PASSWORD-PROTECTED, exit 3

This makes exav safer (see Never silently clean), and it is a new code rather than a reused one, so nothing a script already reads changes meaning: 2 still means the scanner failed. What a CI job has to add is 3, and it should read it as “not a pass”. --partial-as ok restores ClamAV’s answer if that is what the pipeline wants; --clamav-compat sets it for you.

By default exav runs at full capability. To match a stock ClamAV build’s documented limits and extractor set for apples-to-apples differential testing, use --clamav-compat (or set its individual flags). See Differential testing and the CLI reference.

This is the high-value swap for servers and mail gateways: run exav’s daemon on the same socket clamd uses, and clamdscan, milters (clamav-milter, Amavis, Rspamd, MailScanner), and every clamd client library keep working unchanged.

  1. Find clamd’s socket (LocalSocket in clamd.conf):

    Terminal window
    grep -i localsocket /etc/clamav/clamd.conf # e.g. /run/clamav/clamd.ctl
  2. Stop clamd so the socket is free:

    Terminal window
    sudo systemctl stop clamav-daemon
  3. Run exav on that socket, reading the same DB dir:

    Terminal window
    sudo -u clamav exav --listen /run/clamav/clamd.ctl -d /var/lib/clamav

    exav creates the socket 0600, so clients running as another user need the mode the clamd.conf gave them (LocalSocketMode, often 660 alongside a LocalSocketGroup). The mode rides on the address, since it is a property of that socket:

    Terminal window
    grep -i localsocketmode /etc/clamav/clamd.conf # e.g. 660
    sudo -u clamav exav --listen 'clamd:///run/clamav/clamd.ctl?mode=660' \
    -d /var/lib/clamav
  4. Verify from another shell — existing clamd clients work as-is:

    Terminal window
    clamdscan --ping 1
    clamdscan /etc/hosts

Install the provided unit (shipped in the .deb, or copy packaging/exav-clamd.service):

Terminal window
sudo systemctl stop clamav-daemon && sudo systemctl disable clamav-daemon
sudo systemctl enable --now exav-clamd
clamdscan --ping 1

The unit runs as the existing clamav user on the Debian socket path; edit ExecStart / RuntimeDirectory for Fedora/RHEL or to match your clamd.conf. See the daemon guide for the protocol coverage.

Encrypted members are reported PASSWORD-PROTECTED (never a silent clean). Supply passwords to decrypt and scan inside:

Terminal window
exav --passwords secret --passwords hunter2 /data # try a pool
# or a ClamAV .pwdb password database in the signature dir

ZIP (ZipCrypto + WinZip AES), 7z AES-256, encrypted DMG, PDF, and the Office family (legacy XLS and OOXML) are decrypted; RAR AES and PKWARE Strong Encryption are detected but not decrypted. See Encryption support.

  • Signature coverage is ClamAV’s — exav runs those signatures but adds none of its own. A handful (PCRE subsignatures, some bytecode) are skipped and counted, never silently ignored.

  • Large-DB memory is currently higher in exav than ClamAV, by enough to matter: loading a stock freshclam directory (main + daily + bytecode) costs several GB where clamscan on the same files costs a fraction of that. On a host or container without the headroom the loader is OOM-killed, and an OOM kill writes nothing to the log — the process is simply gone, which reads like a crash with no cause. docker inspect --format '{{.State.OOMKilled}}' confirms it.

    Compile the set once with --build-db on a machine with room, and give every scanner the .exavdb: it loads in under a second and a fraction of the memory. A daemon pays the load once for its whole lifetime, so this is mostly a problem for the one-shot clamscan swap, which pays it per invocation. Being worked on.

  • exav is beta: treat non-detections with caution and keep ClamAV as a fallback until you’ve validated coverage on your own corpus (see Differential testing).