Custom ClamAV with real-time monitoring and daily scans
This guide builds a customisable ClamAV setup. It installs the packages, creates the quarantine and log locations, and adds a script that watches your home folders for new files. It also adds log rotation, a background service for the watcher, and an optional daily full scan.
Target audience
Section titled “Target audience”- You dual boot Linux with MS Windows.
- You share files with MS Windows users or have another machine running MS Windows.
- You download a lot of files online.
- You want an extra layer of protection.
What it may not do
Section titled “What it may not do”- It may not protect you from AUR packages or build files with malicious code. See AUR packages.
- ClamAV is open source, so its virus definitions may not be as extensive as those of large commercial vendors.
Real-time monitoring
Section titled “Real-time monitoring”The watcher monitors selected folders. New files are scanned as soon as they are written or moved in. Infected files are moved to a quarantine folder and you get a notification. The setup uses ClamAV, inotify-tools and notify-send.
Run the commands in Bash. The guide switches to Bash because the install commands can fail in Fish.
-
Switch to Bash:
Terminal window bashRun
exitto leave Bash again. -
Install nano, a simple text editor, and the core packages:
Terminal window sudo pacman -S nanosudo pacman -S clamav inotify-tools -
Update the virus definitions:
Terminal window sudo freshclam -
Enable and start the ClamAV updater and daemon:
Terminal window sudo systemctl enable --now clamav-freshclam.servicesudo systemctl enable --now clamav-daemon.service -
Check that the daemon socket exists:
Terminal window ls -l /run/clamav/clamd.ctl -
Create the quarantine folder, set its group to
usersand set its permissions:Terminal window sudo mkdir -p /usr/local/bin/quarantinesudo chgrp users /usr/local/bin/quarantinesudo chmod 757 /usr/local/bin/quarantine757gives the owner full access, the group read and execute access, and other users full access. Other users can therefore delete files in the quarantine folder. -
Create the log folder with the sticky bit set. The sticky bit means only the file owner, the folder owner or root can delete or rename a file:
Terminal window sudo mkdir -p /var/log/clamav-watchsudo chmod 1777 /var/log/clamav-watch -
Create the watcher log file:
Terminal window sudo touch /var/log/clamav-watch/clamav-watch.logsudo chmod 666 /var/log/clamav-watch/clamav-watch.log -
Create the text file that stores the logged-in user name. The watcher script writes to it and the notifier script reads it:
Terminal window sudo touch /usr/local/bin/loggedin_user.txtsudo chgrp users /usr/local/bin/loggedin_user.txtsudo chmod 666 /usr/local/bin/loggedin_user.txt -
Create the watcher script. Save it as
clamav-watch.shinDocuments, then move it to/usr/local/bin. The script watchesDocuments,Downloads,Music,Pictures,Videos,ProjectsandTemplates, and runs a scan at each start. Folders that do not exist are skipped, becauseinotifywaitexits when a listed path is missing.#!/bin/bash# Makes Bash complain if you accidentally use an undefined variable, creates an error in the journal for servicesset -usleep 25echo ${USER} > "/usr/local/bin/loggedin_user.txt"sleep 5# -----------------------● Monitored Folders ●-----------------------# Manually list the folders you want to monitor hereecho "Log: /var/log/clamav-watch/clamav-watch.log"echo "Quarantine Folder: /usr/local/bin/quarantine"# Allow time for initialisation of the file systemHOME_DIR="/home/$USER"WATCH_DIRS=("$HOME_DIR/Documents""$HOME_DIR/Downloads""$HOME_DIR/Music""$HOME_DIR/Pictures""$HOME_DIR/Videos""$HOME_DIR/Projects""$HOME_DIR/Templates")# -----------------------● Variables ●-----------------------# Quarantine folder pathQUARANTINE_DIR="/usr/local/bin/quarantine"# ClamAV log file pathWATCH_LOG_FILE="/var/log/clamav-watch/clamav-watch.log"# Define IGNORE_LIST variable (use the file path)IGNORE_LIST="/etc/clamav/local.ign2"# -----------------------● Startup Scanner ●-----------------------echo "Starting ClamAV monitor on specific folders..." | tee -a "$WATCH_LOG_FILE"echo "Monitoring: ${WATCH_DIRS[*]}" | tee -a "$WATCH_LOG_FILE"echo "" | tee -a "$WATCH_LOG_FILE"# Check if scan on start is enabledif [[ "${SCAN_ON_START}" == "1" ]]thenecho "$(date): Running initial scan on startup..." | tee -a "$WATCH_LOG_FILE"for DIR in "${WATCH_DIRS[@]}"doif [[ -d "$DIR" ]]thenecho "Scanning directory: $DIR" | tee -a "$WATCH_LOG_FILE"RESULT=$(clamdscan --fdpass --multiscan "$DIR" 2>&1)echo "$RESULT" >> "$WATCH_LOG_FILE"if [[ "$RESULT" == *FOUND* ]]then# Extract file paths (only lines with "FOUND")INFECTED_FILES=$(echo "$RESULT" | grep "FOUND" | cut -d: -f1)# Read one path per line, so file names with spaces stay intactwhile IFS= read -r INFECTED_FILEdo# Check Ignore Listif grep -Fxq "$INFECTED_FILE" "$IGNORE_LIST" || grep -Fxq "$(basename "$INFECTED_FILE")" "$IGNORE_LIST"thenecho "Ignored: $INFECTED_FILE" | tee -a "$WATCH_LOG_FILE"continuefi# Move the file manuallyif [[ -f "$INFECTED_FILE" ]]thenmv "$INFECTED_FILE" "$QUARANTINE_DIR/"# Set permissions on the MOVED fileQ_FILE="$QUARANTINE_DIR/$(basename "$INFECTED_FILE")"if [[ -f "$Q_FILE" ]]thenchmod 600 "$Q_FILE"fiecho "Quarantined: $INFECTED_FILE" | tee -a "$WATCH_LOG_FILE"fidone <<< "$INFECTED_FILES"# Send single notificationnotify-send -u critical "🚨 ClamAV Alert" "Scan complete for $(basename "$DIR")\n🦠 Virus infections found & quarantined! 🦠"fifidonefiecho "" | tee -a "$WATCH_LOG_FILE"# -----------------------● Realtime Folder Monitoring ●-----------------------# Monitor all listed folders simultaneously# -m: monitor indefinitely# -r: recursive (optional, remove if you only want top-level)# -e close_write,moved_to: trigger only when file is fully written or moved in# --format '%w%f': output full path# inotifywait exits if a listed folder is missing, so keep only existing foldersEXISTING_DIRS=()for DIR in "${WATCH_DIRS[@]}"doif [[ -d "$DIR" ]]thenEXISTING_DIRS+=("$DIR")elseecho "Skipping missing folder: $DIR" | tee -a "$WATCH_LOG_FILE"fidoneif [[ ${#EXISTING_DIRS[@]} -eq 0 ]]thenecho "No monitored folder exists, exiting." | tee -a "$WATCH_LOG_FILE"exit 1fiinotifywait -m -r -e close_write,moved_to --format '%w%f' "${EXISTING_DIRS[@]}" | while IFS= read -r FILEdo# Skip empty lines or system temp filesif [[ -n "$FILE" && ! "$FILE" =~ \.ldiff$ ]]thenecho "" | tee -a "$WATCH_LOG_FILE"echo "$(date): Detected change: $FILE" | tee -a "$WATCH_LOG_FILE"# Scan using clamdscan (faster, uses daemon)# Scan first WITHOUT movingRESULT=$(clamdscan --fdpass --multiscan "$FILE" 2>&1)echo "$RESULT" >> "$WATCH_LOG_FILE"if [[ "$RESULT" == *FOUND* ]]then# Check Ignore List FIRST (before moving)if grep -Fxq "$FILE" "$IGNORE_LIST" || grep -Fxq "$(basename "$FILE")" "$IGNORE_LIST"thenecho "Ignored: $FILE" | tee -a "$WATCH_LOG_FILE"continueelse# Move the file to quarantinemv "$FILE" "$QUARANTINE_DIR/"# Set the file permissions to non-executableQ_FILE="$QUARANTINE_DIR/$(basename "$FILE")"chmod 600 "$Q_FILE"notify-send -u critical "🚨 ClamAV Alert" "🦠 Virus infections found & quarantined! 🦠\n$(basename "$FILE")"fifi# Alternative: Use clamscan if daemon is not running# clamscan --move="${QUARANTINE_DIR}" "$FILE" >> "$WATCH_LOG_FILE" 2>&1fidone -
Make the watcher script executable and move it into place:
Terminal window sudo chmod 755 clamav-watch.shsudo mv clamav-watch.sh /usr/local/bin/ -
Create the log rotation rule for the watcher log. When the log reaches 1 MB it is compressed and renamed, and 5 rotated copies are kept.
Terminal window sudo nano /etc/logrotate.d/clamav-watch/var/log/clamav-watch/clamav-watch.log {size 1Mrotate 5compressmissingoknotifempty# Ensures the new fresh log file created after rotation, has 666 permissions so users can continue writing.create 666 root root# su = user and group that logrotate runs as when rotating this logsu root rootpostrotate# Optional: Signal your service to reopen the file if needed# systemctl reload your-serviceendscript}Press
Ctrl+Sto save andCtrl+Xto close the file. -
Create the user service that runs the watcher at login. The file goes in
/etc/systemd/user, which is the location used for this step:Terminal window sudo nano /etc/systemd/user/clamav-watch.service[Unit]Description=ClamAV Real‑Time File Scanner# Ensure this starts AFTER the graphical session is ready so notifications work#After=graphical-session.target# Ties its life cycle to that session.#PartOf=graphical-session.target# Ensures the ClamAV daemon is active before this script starts (required for clamdscan)Wants=clamav-daemon.service#After=clamav-daemon.service network.targetAfter=default.target# Optional: Explicitly requires the daemon, fails if clamav-daemon is not running#Requires=clamav-daemon.service[Service]# Defines the process type, 'simple' means the ExecStart process is the main processType=simple# %h expands to the specific user's home directory automatically# %u expands to the username#Environment=HOME=%h#Environment=USER=%uEnvironment="SCAN_ON_START=1"# Set to 1 to scan, 0 to skip# --- Restart policy options ---# Defines restart behavior, 'always' restarts the service if it crashes or exits unexpectedly#Restart=on-failure#Restart=alwaysRestart=no#Type=oneshot# The command to execute, runs the custom monitoring scriptExecStart=/usr/local/bin/clamav-watch.sh# Short delay before attempting a restart to prevent rapid crash loops#RestartSec=5# Sends service output to the systemd journal (allows viewing logs via 'journalctl --user -u clamav-watch')# View live service logs 'journalctl --user -u clamav-watch -f'# View your custom scan log: 'tail -f /var/log/clamav-watch/clamav-watch.log'StandardOutput=journalStandardError=journal# Increases the limit for open file descriptors, crucial when monitoring many folders recursivelyLimitNOFILE=65536[Install]# --- Alternative targets ---# Defines the target that wants this service, 'multi-user.target' enables it for normal multi-user bootWantedBy=default.target# Makes it start as part of the graphical desktop target.#WantedBy=graphical.target#WantedBy=multi-user.targetPress
Ctrl+Sto save andCtrl+Xto close the file. -
Set the file permissions, reload systemd and enable the service for all users:
Terminal window sudo chmod 644 /etc/systemd/user/clamav-watch.servicesudo systemctl daemon-reexecsudo systemctl daemon-reloadsudo systemctl --global enable clamav-watch.servicesudo systemctl daemon-reexecsudo systemctl daemon-reloadsystemctl --user start clamav-watch.service -
Create the exclusion file. Add the file names or package names that should not be scanned:
Terminal window sudo nano /etc/clamav/local.ign2ms-python.debugpy-2025.14.1-linux-x64 # Example format:# ms-python.vscode-python-envs-1.10.0-universalPress
Ctrl+Sto save andCtrl+Xto close the file.
Real-time monitoring is complete.
Scheduled virus scans
Section titled “Scheduled virus scans”The daily scan runs at a time you choose and uses a low CPU priority, so you can keep using the computer. The scan sends notifications when it starts, when it finishes and when it finds an infection. The notifications look the same as those of the real-time monitor.
Here is an example of the real-time infection notification:

-
Create the notifier script. It reads the logged-in user name from
loggedin_user.txtand sends the notification to that user.Terminal window sudo nano /usr/local/bin/clamscan_notify.sh#!/bin/bash# Makes Bash complain if you accidentally use an undefined variable, creates an error in the journal for servicesset -uMESSAGE="${1}"# Return code from the clamscan application: ( :-default ) Means if no value use and empty stringRET_CODE="${2:-default}"VIRUS_COUNT="${3:-default}"read -r LOGGEDIN_USER < /usr/local/bin/loggedin_user.txt# A root system service has no session bus, so point notify-send at the user's busBUS_ADDRESS="unix:path=/run/user/$(id -u "$LOGGEDIN_USER")/bus"if [[ "$MESSAGE" == "START" ]]thenrunuser -u "$LOGGEDIN_USER" -- env DBUS_SESSION_BUS_ADDRESS="$BUS_ADDRESS" notify-send -u critical "🔔 ClamAV Scan" "Starting scheduled virus scan..."exit 0elif [[ "$MESSAGE" == "FINISHED" ]]thenrunuser -u "$LOGGEDIN_USER" -- env DBUS_SESSION_BUS_ADDRESS="$BUS_ADDRESS" notify-send -u critical "🔔 ClamAV Scan" "Scheduled virus scan completed successfully!"exit 0elif [[ "$MESSAGE" == "VIRUS_FOUND" ]]thenrunuser -u "$LOGGEDIN_USER" -- env DBUS_SESSION_BUS_ADDRESS="$BUS_ADDRESS" notify-send -u critical "🚨 ClamAV Scan" "🦠 Virus infections found & quarantined! 🦠\nPlease check the log file /var/log/clamav-watch/clamscan.log\n$VIRUS_COUNT"exit 0elif [[ "$MESSAGE" == "FAILED" ]]thenrunuser -u "$LOGGEDIN_USER" -- env DBUS_SESSION_BUS_ADDRESS="$BUS_ADDRESS" notify-send -u critical "❌ ClamAV Scan" "Scheduled virus scan failed! \nCode: $RET_CODE"exit 0elif [[ "$MESSAGE" == "UNKNOWN_CODE" ]]thenrunuser -u "$LOGGEDIN_USER" -- env DBUS_SESSION_BUS_ADDRESS="$BUS_ADDRESS" notify-send -u critical "⁉️ ClamAV Scan" "Unaccounted for return code! \nCode: $RET_CODE"exit 0fi# Notify-send Info:# Urgency Level Behavior# low Passive, often hidden or times out quickly.# normal Standard popup, times out automatically (default).# critical Stays on screen until manually dismissed, may interrupt.Press
Ctrl+Sto save andCtrl+Xto close the file. -
Make the notifier executable, create its log file and set the log permissions:
Terminal window sudo chmod +x /usr/local/bin/clamscan_notify.shsudo touch /var/log/clamav-watch/clamscan.logsudo chmod 666 /var/log/clamav-watch/clamscan.log -
Create the log rotation rule for the scan log:
Terminal window sudo nano /etc/logrotate.d/clamscan-schedule/var/log/clamav-watch/clamscan.log {size 1Mrotate 5compressmissingoknotifempty# Ensures the new fresh log file created after rotation, has 666 permissions so users can continue writing.create 666 root root# su = user and group that logrotate runs as when rotating this logsu root rootpostrotate# Optional: Signal your service to reopen the file if needed# systemctl reload your-serviceendscript}Press
Ctrl+Sto save andCtrl+Xto close the file. -
Create the scan script. It runs
clamscanon/home, moves infected files to the quarantine folder and sends a notification with the result.Terminal window sudo nano /usr/local/bin/clamscan.sh#!/bin/bash# Makes Bash complain if you accidentally use an undefined variable, creates an error in the journal for servicesset -u# mktemp command to make a temp file prevents race conditionsTEMP_LOG=$(mktemp)# trap means this command to remove the temp file when the script exitstrap 'rm -f "$TEMP_LOG"' EXIT/usr/local/bin/clamscan_notify.sh "START"clamscan -r --exclude-dir=^/sys --exclude-dir=^/proc --exclude-dir=^/dev --move=/usr/local/bin/quarantine -l "$TEMP_LOG" /home# Return code from the clamscan applicationRET_CODE=$?# Pipe scan output to the log filecat "$TEMP_LOG" >> /var/log/clamav-watch/clamscan.logif [ "$RET_CODE" -eq 0 ]then/usr/local/bin/clamscan_notify.sh "FINISHED" "$RET_CODE"elif [ "$RET_CODE" -eq 1 ]thenVIRUS_COUNT=$(grep "Infected files" "$TEMP_LOG" | tail -n1)/usr/local/bin/clamscan_notify.sh "VIRUS_FOUND" "$RET_CODE" "$VIRUS_COUNT"elif [ "$RET_CODE" -eq 2 ]then/usr/local/bin/clamscan_notify.sh "FAILED" "$RET_CODE"elif [ "$RET_CODE" -gt 2 ]then/usr/local/bin/clamscan_notify.sh "UNKNOWN_CODE" "$RET_CODE"fiexit "$RET_CODE"# /usr/bin/clamscan The clamscan command# -r Recursive - scan directories and all subdirectories# --exclude-dir=^/sys Skip /sys directory (kernel/system files)# --exclude-dir=^/proc Skip /proc directory (process info)# --exclude-dir=^/dev Skip /dev directory (device files)# --move=/usr/local/bin/quarantine Move infected files to quarantine folder# /home Scan this directory# Useful clamscan return code behavior, common convention is:# 0 = no virus found,# 1 = virus found,# 2 = scanner or operational error.Press
Ctrl+Sto save andCtrl+Xto close the file. -
Make the scan script executable:
Terminal window sudo chmod +x /usr/local/bin/clamscan.sh -
Create the scan service.
Nice=10sets the CPU priority. Higher values use less CPU.Terminal window sudo nano /etc/systemd/system/clamscan.service[Unit]Description=Clamscan Schedule Service[Service]# Runs the command once, then exits. Service is considered "done" when the command finishes.Type=oneshot# Root is implied by default for system services#User=rootNice=10# Nice Value Priority Effect# -20 Highest priority Uses CPU aggressively# 0 Normal priority Default# 10 Lower priority Uses CPU less aggressively# 19 Lowest priority Very gentle on CPU# Increases the limit for open file descriptors, crucial when monitoring many folders recursivelyLimitNOFILE=65536# Starts the clamscan scriptExecStart=/usr/local/bin/clamscan.shPress
Ctrl+Sto save andCtrl+Xto close the file. -
Create the timer.
OnCalendarsets the time in 24 hour format,HH:MM:SS. The default is 04:00 every day.RandomizedDelaySecadds up to 15 minutes so that timers do not all fire at once.AccuracySec=1mkeeps the extra delay from timer coalescing to one minute. With a value such as1h, the scan can start up to one more hour late.Terminal window sudo nano /etc/systemd/system/clamscan.timer[Unit]Description=Schedule Daily ClamAV Scan[Timer]# Set the desired time (e.g., 03:00)# Add user input for the scheduled scanOnCalendar=*-*-* 04:00:00#TimeZone=America/New_YorkPersistent=trueAccuracySec=1mRandomizedDelaySec=15m[Install]WantedBy=timers.target graphical.targetPress
Ctrl+Sto save andCtrl+Xto close the file. Choose a time when the computer is usually on and logged in. -
Reload systemd, then enable and start the timer:
Terminal window sudo systemctl daemon-reloadsudo systemctl enable --now clamscan.timer -
Check that the timer is listed. The LEFT column shows the time until the next run. The LAST column stays empty until the first run.
Terminal window sudo systemctl list-timerssudo systemctl status clamscan.timer -
Reinitialise freshclam, the updater. Stop the service, update the definitions, then start the service again:
Terminal window sudo systemctl stop clamav-freshclam.servicesudo freshclamsudo systemctl start clamav-freshclam.service
Testing
Section titled “Testing”-
Open the EICAR test file page and copy the text. Your browser may warn about the site.
-
Save the text in a text file, for example
Eicar_Test_Virus.txt, on your desktop. -
Real-time watcher: copy the test file into one of the monitored folders, for example
Documents. The watcher should find it, move it to the quarantine folder and remove its permissions. You should get an infection notification. -
Scheduled scan: start the scan now with the service:
Terminal window sudo systemctl start clamscan.serviceTo test the timer instead, edit
/etc/systemd/system/clamscan.timerand setOnCalendarto a few minutes from now. Then runsudo systemctl daemon-reloadandsudo systemctl restart clamscan.timer. -
Check that the test file is gone from its original location, then check the quarantine folder. Delete the test file there. Check
/var/log/clamav-watch/clamscan.logfor the scan results.
File and folder paths
Section titled “File and folder paths”| Type | Path |
|---|---|
| Watcher script | /usr/local/bin/clamav-watch.sh |
| Scan script | /usr/local/bin/clamscan.sh |
| Notifier script | /usr/local/bin/clamscan_notify.sh |
| Watcher user service | /etc/systemd/user/clamav-watch.service |
| Scan service | /etc/systemd/system/clamscan.service |
| Scan timer | /etc/systemd/system/clamscan.timer |
| Watcher log | /var/log/clamav-watch/clamav-watch.log |
| Scan log | /var/log/clamav-watch/clamscan.log |
| Log rotation | /etc/logrotate.d/clamav-watch, /etc/logrotate.d/clamscan-schedule |
| Quarantine folder | /usr/local/bin/quarantine |
| Exclusion list | /etc/clamav/local.ign2 |
| Logged-in user file | /usr/local/bin/loggedin_user.txt |
Handy commands
Section titled “Handy commands”Update the virus definitions:
The database folder belongs to the clamav user, so run freshclam with sudo:
sudo freshclamsudo freshclam -vCheck when the database was last updated:
sudo grep "^Database updated" /var/log/clamav/freshclam.log | tail -n 1Or check /var/log/clamav/freshclam.log.
Scan a file or folder with clamscan:
clamscan /path/to/file.txtclamscan -r /path/to/directoryclamscan -r -i /path/to/directoryclamscan -r --log=/path/to/logfile.log /path/to/directoryScan with the daemon, clamdscan, which is faster:
clamdscan /path/to/file.txtclamdscan --fdpass --multiscan /path/to/directoryclamdscan --fdpass --multiscan --move=/usr/local/bin/quarantine /path/to/directoryFull system scan:
sudo clamscan -r -i --exclude-dir="^/sys|^/proc|^/dev" /sudo clamdscan --fdpass --multiscan -i /clamdscan always scans folders recursively and ignores clamscan-only options such as -r and --exclude-dir. To skip folders with the daemon, add ExcludePath lines to /etc/clamav/clamd.conf, for example ExcludePath ^/proc/, then restart clamav-daemon.
Daemon management:
clamd --debugsudo systemctl reload clamav-daemonsudo systemctl restart clamav-daemonsudo systemctl status clamav-daemonsudo pkill clamdscanVersions:
clamscan -Vfreshclam --versionLogs and timers:
journalctl -u clamscan.servicesystemctl list-timers --allWeb links
Section titled “Web links”Complete setup script
Section titled “Complete setup script”The original guide also contains a complete setup script. It asks whether to install the daily scan and can print the handy commands. It contains the same scripts as this page, so it has the same shell syntax problems that this page fixes. Check it before you run it:
Source: How To Setup A Custom ClamAV Antivirus, Real-Time Monitoring & Daily Scans