Skip to content

Monitoring directory changes and running a script (using systemd.path and inotifywait)


systemd.path and inotifywait

In many cases it is useful to monitor changes in a directory, for example to:

  • send a notification to Discord
  • perform a backup after a change
  • run automatic CI/CD scripts
  • integrate with monitoring systems

I will present two methods here:

  • 1️⃣ systemd.path - simple directory monitoring
  • 2️⃣ inotifywait - full recursive monitoring

systemd.path - a simple method for the main directory

systemd since version 215 has .path unit types, which allow monitoring of a file or directory. At the time of writing this article, systemd is at version 257, so this feature is stable.

How does it work?

  • PathModified=/path - detects changes in the given directory (e.g. adding/removing a file or subdirectory)
  • does not monitor recursively - it will not detect changes inside subdirectories

Note

PathModified monitors changes to the directory itself, meaning:

- creating a new file
- deleting a file
- creating a new subdirectory
- deleting a subdirectory

It will not detect changes inside files within subdirectories.

Example: directory without subdirectories

Let’s assume we have the directory /var/www/html/.

Inside it, for example:

/var/www/html/
├── file1
├── file2
└── file3

Implementation step by step

Notification script

File /usr/local/bin/discord_notify.sh:

#!/bin/bash

source ~/.bashrc
directory="/var/www/html"

curl -H "Content-Type: application/json" -X POST -d "{\"content\": \"Change detected in $directory\"}" "$DISCORD_WEBHOOK_URL"
Setting the environment variable
Set the environment variable `DISCORD_WEBHOOK_URL` in `~/.bashrc` or another appropriate place.
For environment variables, you can also use other files/methods — `.bashrc` may not always work.
If you use an `EnvironmentFile` in the unit configuration, `.bashrc` will not be read due to how systemd launches services.
Linux offers many options here — variables can be defined on the server itself or in more complex systems like GitHub Actions, GitLab CI/CD, Jenkins, etc.
The example shown is just one of many possible approaches.

Make it executable:

chmod +x /usr/local/bin/discord_notify.sh

.service unit

File /etc/systemd/system/discord-notify.service:

[Unit]
Description=Sending Discord notification on changes in directory

[Service]
Type=oneshot
ExecStart=/usr/local/bin/discord_notify.sh

[Install]
WantedBy=multi-user.target

.path unit

File /etc/systemd/system/discord-notify.path:

[Unit]
Description=Checking changes in /var/www/html

[Path]
PathModified=/var/www/html

[Install]
WantedBy=multi-user.target

Starting the service

systemctl daemon-reload
systemctl enable --now discord-notify.path

Test

systemd.path monitoring

Example operations and results:

Operation Will the script run?
touch /var/www/html/file1 ✅ YES
rm /var/www/html/file1 ✅ YES
mkdir /var/www/html/dirxx ✅ YES
touch /var/www/html/dirxx/file1 ❌ NO
rm /var/www/html/dirxx/file1 ❌ NO

Warning

systemd.path does not monitor changes inside subdirectories.
A change in /var/www/html/dirxx/file.txt will not trigger the script.


How PathExistsGlob works

The PathExistsGlob directive allows specifying a pattern (glob) that systemd will check. If at least one file or directory matches the pattern, the linked .service will be triggered.

The mechanism works like the glob() function in bash:

  • * — any string of characters (but only one directory level)
  • e.g. /opt/www/* — everything in the /opt/www/ directory (assuming the directory is not empty)
  • e.g. /backup/*.tar.gz — all .tar.gz files in the /backup/ directory

Limitations

  • PathExistsGlob does not track file changes (such as modification of contents) — it only checks whether a matching file or directory exists.
  • Recursive patterns such as ** (e.g. /opt/www/**/*.txt) are not supported in systemd.path. If you need recursive change tracking, inotifywait is a better option.

Example usage

Let’s say we have a directory /var/incoming/, where a process or script drops .done files.

[Unit]
Description=Check if .done exists in /var/incoming

[Path]
PathExistsGlob=/var/incoming/*.done

[Install]
WantedBy=multi-user.target

As soon as a new *.done file appears, for example:

/var/incoming/dataset_2025_06_07.done

the linked .service will be triggered.

Another example — /var/www/ directory

PathExistsGlob=/opt/www/*

Triggers the .service when any file or directory appears in /opt/www/:

  • any file, e.g. index.html
  • any directory, e.g. images/

PathExistsGlob=/opt/www/* works only on the immediate children of /var/www/. Changes within subdirectories (/opt/www/images/photo.jpg) are not directly tracked — if a subdirectory is created (mkdir images), it will trigger.

Summary

PathExistsGlob is a quick way to react to the presence of files or directories:

  • ✅ simple directory monitoring
  • ✅ reacting to new files
  • ❌ no support for full recursion
  • ❌ no tracking of modifications or deletions of files

For full recursive monitoring of a directory and file changes, inotifywait is a better choice.


inotifywait - recursive monitoring

If you want to monitor:

  • a directory + subdirectories
  • any changes to files
  • creation/deletion of folders and files

systemd.path is not enough. You will need the tool: inotifywait.

Installation

apt install inotify-tools

or

yum install inotify-tools

Monitoring script

File /usr/local/bin/discord_watch.sh:

#!/bin/bash

WATCH_DIR="/var/www/html"

inotifywait -mrq -e close_write,move,create,delete "$WATCH_DIR" | while read -r path action file; do

curl -H "Content-Type: application/json" -X POST -d "{\"content\": \"Change: ${action} -> ${path}${file}\"}" "$DISCORD_WEBHOOK_URL"

done

Make it executable:

chmod +x /usr/local/bin/discord_watch.sh

systemd unit for discord_watch.sh

File /etc/systemd/system/discord-watch.service:

[Unit]
Description=Recursive monitoring of /var/www/html
After=network.target

[Service]
Type=simple
ExecStart=/usr/local/bin/discord_watch.sh
Restart=always

[Install]
WantedBy=multi-user.target

Starting the service

systemctl daemon-reload
systemctl enable --now discord-watch.service

Test

systemd.path monitoring

Example operations and results:

Operation Will the script run?
touch /var/www/html/new_file ✅ YES
rm /var/www/html/file1 ✅ YES
mkdir /var/www/html/dir1 ✅ YES
touch /var/www/html/dir1/file1 ✅ YES
rm /var/www/html/dir1/file1 ✅ YES

Note

inotifywait allows full recursive monitoring of a directory and all its subdirectories. You can monitor any events, such as: create, delete, move, close_write, attrib (metadata changes).


Summary

Feature systemd.path inotifywait + systemd.service
Built-in to systemd ✅ YES ❌ (requires inotify-tools)
Recursion (subdirectories) ❌ NO ✅ YES
Monitoring file modifications ✅ YES ✅ YES
Monitoring changes in subdirectories ❌ NO ✅ YES
Performance very good very good

When to use what?

  • systemd.path — if the directory has no subdirectories or you are only interested in the directory structure itself
  • inotifywait — if you need full monitoring of an entire directory tree (e.g. /var/www/html or /opt/data etc.)

Practical notes

  • File editors (such as vim, nano) may cause temporary operations which will be seen as create and move. In such cases it’s worth limiting the event types or applying a simple filter in the script.
  • inotifywait works very fast and is low-overhead. For large directories, it’s worth testing its performance.