Watch Directives
By the end of this lesson you will know exactly which watch directive to use for every scenario, understand the critical differences between PathChanged and PathModified, and be able to predict re-trigger behavior to avoid infinite loops.
The [Path] section of a .path unit supports five watch directives. Choosing the right one determines whether your automation is reliable or buggy.
Directive Overview
| Directive | Trigger Condition | Re-triggers Without Change? | Loop Risk |
|---|---|---|---|
PathExists=PATH | The path exists (file or directory) | Yes — loops if the path is not removed | High |
PathExistsGlob=PATTERN | At least one path matching the glob exists | Yes — loops if matches aren't removed | High |
PathChanged=PATH | Path is changed and the writer closes the file descriptor | No — waits for the next change event | Low |
PathModified=PATH | Path is written to (fires on partial writes too) | No — waits for the next write event | Low |
DirectoryNotEmpty=PATH | The directory contains at least one entry | Yes — loops until the directory is empty | Medium |
You can use multiple watch directives in a single .path unit. The service is triggered when any of them match (OR logic, not AND).
[Path]
PathChanged=/etc/nginx/conf.d
PathChanged=/etc/nginx/nginx.conf
This fires when either path changes.
Decision Tree
Use this tree to select the correct directive for your use case:
PathExists=
What It Does
Triggers the service when the specified path (file or directory) exists. If the path already exists when the .path unit starts, it fires immediately.
When To Use
- Signal-file patterns — a CI pipeline creates
deploy.meto trigger deployment. - Wait-for-config — start a service only after an admin uploads a config file.
- One-time setup triggers — run a provisioning script when a setup file appears.
Trigger Behavior
| Scenario | Fires? |
|---|---|
File exists when .path unit is enabled | Yes — immediately |
File is created after .path unit is enabled | Yes |
| File is deleted and re-created | Yes — fires on re-creation |
| Service exits but file still exists | Yes — re-triggers (loop!) |
Loop Prevention
If your service does NOT delete the trigger file, the path unit re-activates it in an infinite loop:
- File exists → service starts
- Service exits → systemd re-checks → file still exists
- Service starts again → infinite loop → journal floods
Your script MUST delete or move the trigger file.
Example
[Unit]
Description=Watch for deploy signal file
[Path]
PathExists=/var/www/html/deploy.me
[Install]
WantedBy=paths.target
[Unit]
Description=Run deployment on signal
[Service]
Type=oneshot
ExecStart=/usr/local/bin/deploy.sh
#!/usr/bin/env bash
set -euo pipefail
echo "[$(date -Is)] Deployment started"
# ... your deployment logic ...
rm /var/www/html/deploy.me # CRITICAL: remove the signal file
echo "[$(date -Is)] Deployment complete"
touch /var/www/html/deploy.me
sleep 2
journalctl -u deploy-trigger.service -n 10 --no-pager
Mar 02 00:01:05 vps deploy.sh[1234]: [2026-03-02T00:01:05+00:00] Deployment started
Mar 02 00:01:08 vps deploy.sh[1234]: [2026-03-02T00:01:08+00:00] Deployment complete
Quick Reference
| Property | Value |
|---|---|
| Pre-existing file triggers immediately | Yes |
| Re-triggers if condition persists after service | Yes |
| Loop risk | High |
| Script responsibility | Delete or move the file |
PathExistsGlob=
What It Does
Triggers the service when at least one file matches the specified glob pattern. If matches already exist when the .path unit starts, it fires immediately.
When To Use
- Type-specific drop folders — process only
.sqlfiles and ignore everything else. - Batch import queues — trigger on
.csv,.json, or.xmlpatterns. - Theme/plugin deployment — trigger on
.zipfiles in a staging folder.
Trigger Behavior
| Scenario | Fires? |
|---|---|
Matching files exist when .path unit is enabled | Yes — immediately |
| A matching file is created | Yes |
| All matching files are removed, then one appears | Yes |
| Service exits but matches still exist | Yes — re-triggers (loop!) |
Loop Prevention
Same rule as PathExists= — your script must delete or move all matching files before exiting, or the service will loop.
Supported Glob Patterns
| Pattern | Matches |
|---|---|
/mnt/import/*.sql | Any .sql file in /mnt/import/ |
/var/www/dropzone/*.zip | Any .zip file in the dropzone |
/tmp/upload/*.{csv,json} | .csv and .json files |
/opt/queue/job-* | Files starting with job- |
Example
[Unit]
Description=Watch for incoming SQL dump files
[Path]
PathExistsGlob=/mnt/import/*.sql
[Install]
WantedBy=paths.target
[Unit]
Description=Import a SQL dump into the database
[Service]
Type=oneshot
User=www-data
ExecStart=/usr/local/bin/import-sql.sh
#!/usr/bin/env bash
set -euo pipefail
FILE=$(ls /mnt/import/*.sql 2>/dev/null | head -1)
[ -z "$FILE" ] && exit 0
echo "[$(date -Is)] Importing: $FILE"
mysql -u root wordpress < "$FILE"
mv "$FILE" /mnt/import/done/ # Move to archive, not delete
echo "[$(date -Is)] Import complete"
cp /tmp/backup.sql /mnt/import/
sleep 3
journalctl -u db-import.service -n 10 --no-pager
Mar 02 01:15:00 vps import-sql.sh[1350]: [2026-03-02T01:15:00+00:00] Importing: /mnt/import/backup.sql
Mar 02 01:15:08 vps import-sql.sh[1350]: [2026-03-02T01:15:08+00:00] Import complete
Quick Reference
| Property | Value |
|---|---|
| Pre-existing matches trigger immediately | Yes |
| Re-triggers if matches remain after service | Yes |
| Loop risk | High |
| Script responsibility | Delete or move all matching files |
PathChanged=
What It Does
Triggers the service when the watched path is changed and the writing process closes the file descriptor. This is the safest directive for monitoring file uploads and config edits.
When To Use
- SFTP/SCP uploads — waits for the file to be fully transferred before processing.
- Config file changes — reload a service after an admin edits a config.
- Database file updates — sync a database after it's been fully written.
- Security monitoring — detect changes to critical files like
wp-config.php.
Trigger Behavior
| Scenario | Fires? |
|---|---|
File exists when .path unit is enabled | No — waits for the next change |
| File is created (new file) | Yes |
| File content is modified and fd is closed | Yes |
| File is being written (partial write, fd still open) | No — waits for close |
| Service exits | Waits for next change — no re-trigger |
Why PathChanged is Safe for Uploads
When a file is uploaded via SFTP, the server writes it in chunks:
Chunk 1 → write → fd open
Chunk 2 → write → fd open
Chunk 3 → write → fd open
...
Transfer complete → fd CLOSED → PathChanged fires
Your service only sees the complete file, never a partial upload.
Editor Behavior Caveat
Some text editors (like vim in default mode) write to a temporary swap file and then rename it over the original. This means:
- The original file descriptor is never closed in the expected way.
PathChanged=may not fire for edits made with these editors.
Workaround: Watch the directory instead of the file, or use PathModified=.
# Watch the directory instead
PathChanged=/etc/nginx/conf.d
Example
[Unit]
Description=Watch Nginx conf.d for changes
[Path]
PathChanged=/etc/nginx/conf.d
Unit=nginx-reload.service
[Install]
WantedBy=paths.target
[Unit]
Description=Reload Nginx after config change
[Service]
Type=oneshot
ExecStart=/usr/bin/nginx -t
ExecStart=/bin/systemctl reload nginx
echo "# test change" >> /etc/nginx/conf.d/default.conf
sleep 1
journalctl -u nginx-reload.service -n 5 --no-pager
Mar 02 00:20:01 vps nginx[1400]: nginx: configuration file /etc/nginx/nginx.conf test is successful
Mar 02 00:20:01 vps systemd[1]: Reloading nginx.service...
Quick Reference
| Property | Value |
|---|---|
| Pre-existing file triggers immediately | No |
| Re-triggers after service exits | No |
| Loop risk | Low |
| Script responsibility | None (no cleanup needed) |
PathModified=
What It Does
Triggers the service the instant a write occurs on the watched path — even partial writes. Unlike PathChanged=, it does NOT wait for the file descriptor to close.
When To Use
- Real-time log alerting — send an alert the instant a log receives a new line.
- Instant notification — react immediately to any file modification.
- Debug and development — catch every write event during testing.
When NOT To Use
PathModified= fires on the first network packet of an SFTP upload. Your script will open and process an incomplete file. Use PathChanged= for uploads.
Trigger Behavior
| Scenario | Fires? |
|---|---|
File exists when .path unit is enabled | No — waits for the next write |
| File is written to (even partially) | Yes — instantly |
| Multiple rapid writes | Fires on each write (rate-limit recommended) |
| Service exits | Waits for next write — no re-trigger |
PathChanged vs PathModified — Side by Side
| Aspect | PathChanged= | PathModified= |
|---|---|---|
| Fires when | File is written and fd is closed | File is written to (any write) |
| Safe for SFTP uploads | Yes | No (partial file risk) |
| Latency | Slightly delayed (waits for close) | Instant |
| Best for | Uploads, config edits, database files | Log monitoring, instant alerts |
| Editor compatibility | May miss vim edits | Catches all writes |
Example
[Unit]
Description=Watch urgent.log for new entries
[Path]
PathModified=/var/log/urgent.log
[Install]
WantedBy=paths.target
[Unit]
Description=Send alert on urgent log entry
[Service]
Type=oneshot
ExecStart=/usr/local/bin/send-alert.sh /var/log/urgent.log
echo "CRITICAL: Disk 98% full" >> /var/log/urgent.log
sleep 1
journalctl -u urgent-alert.service -n 5 --no-pager
Mar 02 00:10:01 vps send-alert.sh[1301]: Alert sent via Slack: CRITICAL: Disk 98% full
Quick Reference
| Property | Value |
|---|---|
| Pre-existing file triggers immediately | No |
| Re-triggers after service exits | No |
| Loop risk | Low |
| Script responsibility | None (no cleanup needed) |
DirectoryNotEmpty=
What It Does
Triggers the service whenever the watched directory contains at least one file or subdirectory. If the directory already contains files when the .path unit starts, it fires immediately.
When To Use
- Drop-folder queues — process uploaded files one at a time.
- Image optimization pipelines — optimize files placed in a queue folder.
- CSV/data import queues — process incoming data files sequentially.
Trigger Behavior
| Scenario | Fires? |
|---|---|
Directory has files when .path unit is enabled | Yes — immediately |
| A file is dropped into the directory | Yes |
| Service processes one file but others remain | Yes — re-triggers immediately |
| Service empties the directory | No — waits for next file |
The Queue Processing Pattern
DirectoryNotEmpty= is designed for queue processing. The pattern is:
- File drops into directory → service starts.
- Service processes one file, deletes/moves it, exits.
- systemd checks if directory is still non-empty.
- If yes → service starts again (step 2).
- If no → path unit waits for the next file.
Loop Prevention
Unlike PathChanged=, this directive WILL loop if files remain in the directory. Your script must:
- Process at least one file per run
- Delete or move the processed file before exiting
If the script fails silently and doesn't remove the file, you get an infinite loop.
Using MakeDirectory
DirectoryNotEmpty= pairs well with MakeDirectory=yes — if the watched directory doesn't exist, systemd creates it for you:
[Path]
DirectoryNotEmpty=/var/www/drop
MakeDirectory=yes
DirectoryMode=0775
Example
[Unit]
Description=Watch image optimization queue
[Path]
DirectoryNotEmpty=/var/media/queue
MakeDirectory=yes
DirectoryMode=0775
[Install]
WantedBy=paths.target
[Unit]
Description=Optimize one image from the queue
[Service]
Type=oneshot
User=www-data
ExecStart=/usr/local/bin/optimize-image.sh
#!/usr/bin/env bash
set -euo pipefail
FILE=$(ls /var/media/queue/*.{jpg,png,webp} 2>/dev/null | head -1)
[ -z "$FILE" ] && exit 0
echo "[$(date -Is)] Optimizing: $FILE"
# ... optimization logic ...
mv "$FILE" /var/media/done/
echo "[$(date -Is)] Done"
cp photo1.jpg photo2.png /var/media/queue/
sleep 3
journalctl -u image-queue.service -n 20 --no-pager
Mar 02 02:00:01 vps optimize-image.sh[1500]: [2026-03-02T02:00:01+00:00] Optimizing: /var/media/queue/photo1.jpg
Mar 02 02:00:02 vps optimize-image.sh[1500]: [2026-03-02T02:00:02+00:00] Done
Mar 02 02:00:03 vps optimize-image.sh[1501]: [2026-03-02T02:00:03+00:00] Optimizing: /var/media/queue/photo2.png
Mar 02 02:00:04 vps optimize-image.sh[1501]: [2026-03-02T02:00:04+00:00] Done
Quick Reference
| Property | Value |
|---|---|
| Pre-existing files trigger immediately | Yes |
| Re-triggers if directory still non-empty | Yes |
| Loop risk | Medium |
| Script responsibility | Delete or move at least one file |
Works with MakeDirectory= | Yes |
Pre-Existing Files Summary
This is a common source of confusion. Here's the definitive reference:
| Directive | Fires Immediately for Pre-existing Content? |
|---|---|
PathExists= | Yes — triggers immediately if the file already exists |
PathExistsGlob= | Yes — triggers immediately if matches exist |
DirectoryNotEmpty= | Yes — triggers immediately if directory is not empty |
PathChanged= | No — waits for the next change event |
PathModified= | No — waits for the next write event |
- Need to react to existing content? Use
PathExists=orDirectoryNotEmpty=. - Need to react only to future changes? Use
PathChanged=orPathModified=.
Multiple Directives in One Unit
You can combine directives. The service fires when any directive matches:
[Path]
# Watch critical WordPress files for tampering
PathChanged=/var/www/html/wp-config.php
PathChanged=/var/www/html/.htaccess
PathChanged=/var/www/html/wp-settings.php
You can also mix directive types:
[Path]
PathExists=/var/www/html/deploy.me
DirectoryNotEmpty=/var/www/html/queue/
PathChanged=/var/www/html/.env
When mixing directives with different loop behaviors, the service must handle cleanup for ALL of them. If you have PathExists= and PathChanged=, the script must delete the PathExists= target file even though PathChanged= doesn't require cleanup.
Key Takeaways
- 5 directives, each with different trigger timing and loop behavior.
- PathChanged= is the safest for file uploads — waits for fd close.
- PathModified= is the fastest — fires instantly on any write.
- PathExists=, PathExistsGlob=, DirectoryNotEmpty= all re-trigger and require cleanup.
- You can combine multiple directives in one unit (OR logic).
- Always match the directive to the use case using the decision tree.
What's Next
- Unit File Anatomy — complete reference for every directive in the
.pathand.serviceunit files.