Skip to content

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.

  • 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.
  • 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.

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.

  1. Switch to Bash:

    Terminal window
    bash

    Run exit to leave Bash again.

  2. Install nano, a simple text editor, and the core packages:

    Terminal window
    sudo pacman -S nano
    sudo pacman -S clamav inotify-tools
  3. Update the virus definitions:

    Terminal window
    sudo freshclam
  4. Enable and start the ClamAV updater and daemon:

    Terminal window
    sudo systemctl enable --now clamav-freshclam.service
    sudo systemctl enable --now clamav-daemon.service
  5. Check that the daemon socket exists:

    Terminal window
    ls -l /run/clamav/clamd.ctl
  6. Create the quarantine folder, set its group to users and set its permissions:

    Terminal window
    sudo mkdir -p /usr/local/bin/quarantine
    sudo chgrp users /usr/local/bin/quarantine
    sudo chmod 757 /usr/local/bin/quarantine

    757 gives 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.

  7. 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-watch
    sudo chmod 1777 /var/log/clamav-watch
  8. Create the watcher log file:

    Terminal window
    sudo touch /var/log/clamav-watch/clamav-watch.log
    sudo chmod 666 /var/log/clamav-watch/clamav-watch.log
  9. 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.txt
    sudo chgrp users /usr/local/bin/loggedin_user.txt
    sudo chmod 666 /usr/local/bin/loggedin_user.txt
  10. Create the watcher script. Save it as clamav-watch.sh in Documents, then move it to /usr/local/bin. The script watches Documents, Downloads, Music, Pictures, Videos, Projects and Templates, and runs a scan at each start. Folders that do not exist are skipped, because inotifywait exits 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 services
    set -u
    sleep 25
    echo ${USER} > "/usr/local/bin/loggedin_user.txt"
    sleep 5
    # -----------------------● Monitored Folders ●-----------------------
    # Manually list the folders you want to monitor here
    echo "Log: /var/log/clamav-watch/clamav-watch.log"
    echo "Quarantine Folder: /usr/local/bin/quarantine"
    # Allow time for initialisation of the file system
    HOME_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 path
    QUARANTINE_DIR="/usr/local/bin/quarantine"
    # ClamAV log file path
    WATCH_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 enabled
    if [[ "${SCAN_ON_START}" == "1" ]]
    then
    echo "$(date): Running initial scan on startup..." | tee -a "$WATCH_LOG_FILE"
    for DIR in "${WATCH_DIRS[@]}"
    do
    if [[ -d "$DIR" ]]
    then
    echo "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 intact
    while IFS= read -r INFECTED_FILE
    do
    # Check Ignore List
    if grep -Fxq "$INFECTED_FILE" "$IGNORE_LIST" || grep -Fxq "$(basename "$INFECTED_FILE")" "$IGNORE_LIST"
    then
    echo "Ignored: $INFECTED_FILE" | tee -a "$WATCH_LOG_FILE"
    continue
    fi
    # Move the file manually
    if [[ -f "$INFECTED_FILE" ]]
    then
    mv "$INFECTED_FILE" "$QUARANTINE_DIR/"
    # Set permissions on the MOVED file
    Q_FILE="$QUARANTINE_DIR/$(basename "$INFECTED_FILE")"
    if [[ -f "$Q_FILE" ]]
    then
    chmod 600 "$Q_FILE"
    fi
    echo "Quarantined: $INFECTED_FILE" | tee -a "$WATCH_LOG_FILE"
    fi
    done <<< "$INFECTED_FILES"
    # Send single notification
    notify-send -u critical "🚨 ClamAV Alert" "Scan complete for $(basename "$DIR")\n🦠 Virus infections found & quarantined! 🦠"
    fi
    fi
    done
    fi
    echo "" | 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 folders
    EXISTING_DIRS=()
    for DIR in "${WATCH_DIRS[@]}"
    do
    if [[ -d "$DIR" ]]
    then
    EXISTING_DIRS+=("$DIR")
    else
    echo "Skipping missing folder: $DIR" | tee -a "$WATCH_LOG_FILE"
    fi
    done
    if [[ ${#EXISTING_DIRS[@]} -eq 0 ]]
    then
    echo "No monitored folder exists, exiting." | tee -a "$WATCH_LOG_FILE"
    exit 1
    fi
    inotifywait -m -r -e close_write,moved_to --format '%w%f' "${EXISTING_DIRS[@]}" | while IFS= read -r FILE
    do
    # Skip empty lines or system temp files
    if [[ -n "$FILE" && ! "$FILE" =~ \.ldiff$ ]]
    then
    echo "" | tee -a "$WATCH_LOG_FILE"
    echo "$(date): Detected change: $FILE" | tee -a "$WATCH_LOG_FILE"
    # Scan using clamdscan (faster, uses daemon)
    # Scan first WITHOUT moving
    RESULT=$(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"
    then
    echo "Ignored: $FILE" | tee -a "$WATCH_LOG_FILE"
    continue
    else
    # Move the file to quarantine
    mv "$FILE" "$QUARANTINE_DIR/"
    # Set the file permissions to non-executable
    Q_FILE="$QUARANTINE_DIR/$(basename "$FILE")"
    chmod 600 "$Q_FILE"
    notify-send -u critical "🚨 ClamAV Alert" "🦠 Virus infections found & quarantined! 🦠\n$(basename "$FILE")"
    fi
    fi
    # Alternative: Use clamscan if daemon is not running
    # clamscan --move="${QUARANTINE_DIR}" "$FILE" >> "$WATCH_LOG_FILE" 2>&1
    fi
    done
  11. Make the watcher script executable and move it into place:

    Terminal window
    sudo chmod 755 clamav-watch.sh
    sudo mv clamav-watch.sh /usr/local/bin/
  12. 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 1M
    rotate 5
    compress
    missingok
    notifempty
    # 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 log
    su root root
    postrotate
    # Optional: Signal your service to reopen the file if needed
    # systemctl reload your-service
    endscript
    }

    Press Ctrl+S to save and Ctrl+X to close the file.

  13. 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.target
    After=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 process
    Type=simple
    # %h expands to the specific user's home directory automatically
    # %u expands to the username
    #Environment=HOME=%h
    #Environment=USER=%u
    Environment="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=always
    Restart=no
    #Type=oneshot
    # The command to execute, runs the custom monitoring script
    ExecStart=/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=journal
    StandardError=journal
    # Increases the limit for open file descriptors, crucial when monitoring many folders recursively
    LimitNOFILE=65536
    [Install]
    # --- Alternative targets ---
    # Defines the target that wants this service, 'multi-user.target' enables it for normal multi-user boot
    WantedBy=default.target
    # Makes it start as part of the graphical desktop target.
    #WantedBy=graphical.target
    #WantedBy=multi-user.target

    Press Ctrl+S to save and Ctrl+X to close the file.

  14. Set the file permissions, reload systemd and enable the service for all users:

    Terminal window
    sudo chmod 644 /etc/systemd/user/clamav-watch.service
    sudo systemctl daemon-reexec
    sudo systemctl daemon-reload
    sudo systemctl --global enable clamav-watch.service
    sudo systemctl daemon-reexec
    sudo systemctl daemon-reload
    systemctl --user start clamav-watch.service
  15. Create the exclusion file. Add the file names or package names that should not be scanned:

    Terminal window
    sudo nano /etc/clamav/local.ign2
    ms-python.debugpy-2025.14.1-linux-x64
    # Example format:
    # ms-python.vscode-python-envs-1.10.0-universal

    Press Ctrl+S to save and Ctrl+X to close the file.

Real-time monitoring is complete.

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:

ClamAV infection notification

  1. Create the notifier script. It reads the logged-in user name from loggedin_user.txt and 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 services
    set -u
    MESSAGE="${1}"
    # Return code from the clamscan application: ( :-default ) Means if no value use and empty string
    RET_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 bus
    BUS_ADDRESS="unix:path=/run/user/$(id -u "$LOGGEDIN_USER")/bus"
    if [[ "$MESSAGE" == "START" ]]
    then
    runuser -u "$LOGGEDIN_USER" -- env DBUS_SESSION_BUS_ADDRESS="$BUS_ADDRESS" notify-send -u critical "🔔 ClamAV Scan" "Starting scheduled virus scan..."
    exit 0
    elif [[ "$MESSAGE" == "FINISHED" ]]
    then
    runuser -u "$LOGGEDIN_USER" -- env DBUS_SESSION_BUS_ADDRESS="$BUS_ADDRESS" notify-send -u critical "🔔 ClamAV Scan" "Scheduled virus scan completed successfully!"
    exit 0
    elif [[ "$MESSAGE" == "VIRUS_FOUND" ]]
    then
    runuser -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 0
    elif [[ "$MESSAGE" == "FAILED" ]]
    then
    runuser -u "$LOGGEDIN_USER" -- env DBUS_SESSION_BUS_ADDRESS="$BUS_ADDRESS" notify-send -u critical "❌ ClamAV Scan" "Scheduled virus scan failed! \nCode: $RET_CODE"
    exit 0
    elif [[ "$MESSAGE" == "UNKNOWN_CODE" ]]
    then
    runuser -u "$LOGGEDIN_USER" -- env DBUS_SESSION_BUS_ADDRESS="$BUS_ADDRESS" notify-send -u critical "⁉️ ClamAV Scan" "Unaccounted for return code! \nCode: $RET_CODE"
    exit 0
    fi
    # 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+S to save and Ctrl+X to close the file.

  2. Make the notifier executable, create its log file and set the log permissions:

    Terminal window
    sudo chmod +x /usr/local/bin/clamscan_notify.sh
    sudo touch /var/log/clamav-watch/clamscan.log
    sudo chmod 666 /var/log/clamav-watch/clamscan.log
  3. 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 1M
    rotate 5
    compress
    missingok
    notifempty
    # 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 log
    su root root
    postrotate
    # Optional: Signal your service to reopen the file if needed
    # systemctl reload your-service
    endscript
    }

    Press Ctrl+S to save and Ctrl+X to close the file.

  4. Create the scan script. It runs clamscan on /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 services
    set -u
    # mktemp command to make a temp file prevents race conditions
    TEMP_LOG=$(mktemp)
    # trap means this command to remove the temp file when the script exits
    trap '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 application
    RET_CODE=$?
    # Pipe scan output to the log file
    cat "$TEMP_LOG" >> /var/log/clamav-watch/clamscan.log
    if [ "$RET_CODE" -eq 0 ]
    then
    /usr/local/bin/clamscan_notify.sh "FINISHED" "$RET_CODE"
    elif [ "$RET_CODE" -eq 1 ]
    then
    VIRUS_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"
    fi
    exit "$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+S to save and Ctrl+X to close the file.

  5. Make the scan script executable:

    Terminal window
    sudo chmod +x /usr/local/bin/clamscan.sh
  6. Create the scan service. Nice=10 sets 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=root
    Nice=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 recursively
    LimitNOFILE=65536
    # Starts the clamscan script
    ExecStart=/usr/local/bin/clamscan.sh

    Press Ctrl+S to save and Ctrl+X to close the file.

  7. Create the timer. OnCalendar sets the time in 24 hour format, HH:MM:SS. The default is 04:00 every day. RandomizedDelaySec adds up to 15 minutes so that timers do not all fire at once. AccuracySec=1m keeps the extra delay from timer coalescing to one minute. With a value such as 1h, 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 scan
    OnCalendar=*-*-* 04:00:00
    #TimeZone=America/New_York
    Persistent=true
    AccuracySec=1m
    RandomizedDelaySec=15m
    [Install]
    WantedBy=timers.target graphical.target

    Press Ctrl+S to save and Ctrl+X to close the file. Choose a time when the computer is usually on and logged in.

  8. Reload systemd, then enable and start the timer:

    Terminal window
    sudo systemctl daemon-reload
    sudo systemctl enable --now clamscan.timer
  9. 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-timers
    sudo systemctl status clamscan.timer
  10. Reinitialise freshclam, the updater. Stop the service, update the definitions, then start the service again:

    Terminal window
    sudo systemctl stop clamav-freshclam.service
    sudo freshclam
    sudo systemctl start clamav-freshclam.service
  1. Open the EICAR test file page and copy the text. Your browser may warn about the site.

  2. Save the text in a text file, for example Eicar_Test_Virus.txt, on your desktop.

  3. 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.

  4. Scheduled scan: start the scan now with the service:

    Terminal window
    sudo systemctl start clamscan.service

    To test the timer instead, edit /etc/systemd/system/clamscan.timer and set OnCalendar to a few minutes from now. Then run sudo systemctl daemon-reload and sudo systemctl restart clamscan.timer.

  5. 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.log for the scan results.

TypePath
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

Update the virus definitions:

The database folder belongs to the clamav user, so run freshclam with sudo:

Terminal window
sudo freshclam
sudo freshclam -v

Check when the database was last updated:

Terminal window
sudo grep "^Database updated" /var/log/clamav/freshclam.log | tail -n 1

Or check /var/log/clamav/freshclam.log.

Scan a file or folder with clamscan:

Terminal window
clamscan /path/to/file.txt
clamscan -r /path/to/directory
clamscan -r -i /path/to/directory
clamscan -r --log=/path/to/logfile.log /path/to/directory

Scan with the daemon, clamdscan, which is faster:

Terminal window
clamdscan /path/to/file.txt
clamdscan --fdpass --multiscan /path/to/directory
clamdscan --fdpass --multiscan --move=/usr/local/bin/quarantine /path/to/directory

Full system scan:

Terminal window
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:

Terminal window
clamd --debug
sudo systemctl reload clamav-daemon
sudo systemctl restart clamav-daemon
sudo systemctl status clamav-daemon
sudo pkill clamdscan

Versions:

Terminal window
clamscan -V
freshclam --version

Logs and timers:

Terminal window
journalctl -u clamscan.service
systemctl list-timers --all

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:

Complete ClamAV setup script


Source: How To Setup A Custom ClamAV Antivirus, Real-Time Monitoring & Daily Scans