Skip to main content

Watch Directives

Learning Focus

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

DirectiveTrigger ConditionRe-triggers Without Change?Loop Risk
PathExists=PATHThe path exists (file or directory)Yes — loops if the path is not removedHigh
PathExistsGlob=PATTERNAt least one path matching the glob existsYes — loops if matches aren't removedHigh
PathChanged=PATHPath is changed and the writer closes the file descriptorNo — waits for the next change eventLow
PathModified=PATHPath is written to (fires on partial writes too)No — waits for the next write eventLow
DirectoryNotEmpty=PATHThe directory contains at least one entryYes — loops until the directory is emptyMedium
Combining Directives

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.me to 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

ScenarioFires?
File exists when .path unit is enabledYes — immediately
File is created after .path unit is enabledYes
File is deleted and re-createdYes — fires on re-creation
Service exits but file still existsYes — re-triggers (loop!)

Loop Prevention

Critical

If your service does NOT delete the trigger file, the path unit re-activates it in an infinite loop:

  1. File exists → service starts
  2. Service exits → systemd re-checks → file still exists
  3. Service starts again → infinite loop → journal floods

Your script MUST delete or move the trigger file.

Example

/etc/systemd/system/deploy-trigger.path
[Unit]
Description=Watch for deploy signal file

[Path]
PathExists=/var/www/html/deploy.me

[Install]
WantedBy=paths.target
/etc/systemd/system/deploy-trigger.service
[Unit]
Description=Run deployment on signal

[Service]
Type=oneshot
ExecStart=/usr/local/bin/deploy.sh
/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"
trigger-test.sh
touch /var/www/html/deploy.me
sleep 2
journalctl -u deploy-trigger.service -n 10 --no-pager
expected-output.txt
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

PropertyValue
Pre-existing file triggers immediatelyYes
Re-triggers if condition persists after serviceYes
Loop riskHigh
Script responsibilityDelete 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 .sql files and ignore everything else.
  • Batch import queues — trigger on .csv, .json, or .xml patterns.
  • Theme/plugin deployment — trigger on .zip files in a staging folder.

Trigger Behavior

ScenarioFires?
Matching files exist when .path unit is enabledYes — immediately
A matching file is createdYes
All matching files are removed, then one appearsYes
Service exits but matches still existYes — re-triggers (loop!)

Loop Prevention

Critical

Same rule as PathExists= — your script must delete or move all matching files before exiting, or the service will loop.

Supported Glob Patterns

PatternMatches
/mnt/import/*.sqlAny .sql file in /mnt/import/
/var/www/dropzone/*.zipAny .zip file in the dropzone
/tmp/upload/*.{csv,json}.csv and .json files
/opt/queue/job-*Files starting with job-

Example

/etc/systemd/system/db-import.path
[Unit]
Description=Watch for incoming SQL dump files

[Path]
PathExistsGlob=/mnt/import/*.sql

[Install]
WantedBy=paths.target
/etc/systemd/system/db-import.service
[Unit]
Description=Import a SQL dump into the database

[Service]
Type=oneshot
User=www-data
ExecStart=/usr/local/bin/import-sql.sh
/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"
trigger-test.sh
cp /tmp/backup.sql /mnt/import/
sleep 3
journalctl -u db-import.service -n 10 --no-pager
expected-output.txt
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

PropertyValue
Pre-existing matches trigger immediatelyYes
Re-triggers if matches remain after serviceYes
Loop riskHigh
Script responsibilityDelete 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

ScenarioFires?
File exists when .path unit is enabledNo — waits for the next change
File is created (new file)Yes
File content is modified and fd is closedYes
File is being written (partial write, fd still open)No — waits for close
Service exitsWaits 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

vim and Some Editors

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

/etc/systemd/system/nginx-conf-watcher.path
[Unit]
Description=Watch Nginx conf.d for changes

[Path]
PathChanged=/etc/nginx/conf.d
Unit=nginx-reload.service

[Install]
WantedBy=paths.target
/etc/systemd/system/nginx-reload.service
[Unit]
Description=Reload Nginx after config change

[Service]
Type=oneshot
ExecStart=/usr/bin/nginx -t
ExecStart=/bin/systemctl reload nginx
trigger-test.sh
echo "# test change" >> /etc/nginx/conf.d/default.conf
sleep 1
journalctl -u nginx-reload.service -n 5 --no-pager
expected-output.txt
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

PropertyValue
Pre-existing file triggers immediatelyNo
Re-triggers after service exitsNo
Loop riskLow
Script responsibilityNone (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

Do NOT Use for Uploads

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

ScenarioFires?
File exists when .path unit is enabledNo — waits for the next write
File is written to (even partially)Yes — instantly
Multiple rapid writesFires on each write (rate-limit recommended)
Service exitsWaits for next write — no re-trigger

PathChanged vs PathModified — Side by Side

AspectPathChanged=PathModified=
Fires whenFile is written and fd is closedFile is written to (any write)
Safe for SFTP uploadsYesNo (partial file risk)
LatencySlightly delayed (waits for close)Instant
Best forUploads, config edits, database filesLog monitoring, instant alerts
Editor compatibilityMay miss vim editsCatches all writes

Example

/etc/systemd/system/urgent-alert.path
[Unit]
Description=Watch urgent.log for new entries

[Path]
PathModified=/var/log/urgent.log

[Install]
WantedBy=paths.target
/etc/systemd/system/urgent-alert.service
[Unit]
Description=Send alert on urgent log entry

[Service]
Type=oneshot
ExecStart=/usr/local/bin/send-alert.sh /var/log/urgent.log
trigger-test.sh
echo "CRITICAL: Disk 98% full" >> /var/log/urgent.log
sleep 1
journalctl -u urgent-alert.service -n 5 --no-pager
expected-output.txt
Mar 02 00:10:01 vps send-alert.sh[1301]: Alert sent via Slack: CRITICAL: Disk 98% full

Quick Reference

PropertyValue
Pre-existing file triggers immediatelyNo
Re-triggers after service exitsNo
Loop riskLow
Script responsibilityNone (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

ScenarioFires?
Directory has files when .path unit is enabledYes — immediately
A file is dropped into the directoryYes
Service processes one file but others remainYes — re-triggers immediately
Service empties the directoryNo — waits for next file

The Queue Processing Pattern

DirectoryNotEmpty= is designed for queue processing. The pattern is:

  1. File drops into directory → service starts.
  2. Service processes one file, deletes/moves it, exits.
  3. systemd checks if directory is still non-empty.
  4. If yes → service starts again (step 2).
  5. If no → path unit waits for the next file.

Loop Prevention

Medium Risk

Unlike PathChanged=, this directive WILL loop if files remain in the directory. Your script must:

  1. Process at least one file per run
  2. 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

/etc/systemd/system/image-queue.path
[Unit]
Description=Watch image optimization queue

[Path]
DirectoryNotEmpty=/var/media/queue
MakeDirectory=yes
DirectoryMode=0775

[Install]
WantedBy=paths.target
/etc/systemd/system/image-queue.service
[Unit]
Description=Optimize one image from the queue

[Service]
Type=oneshot
User=www-data
ExecStart=/usr/local/bin/optimize-image.sh
/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"
trigger-test.sh
cp photo1.jpg photo2.png /var/media/queue/
sleep 3
journalctl -u image-queue.service -n 20 --no-pager
expected-output.txt
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

PropertyValue
Pre-existing files trigger immediatelyYes
Re-triggers if directory still non-emptyYes
Loop riskMedium
Script responsibilityDelete 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:

DirectiveFires 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
Rule of Thumb
  • Need to react to existing content? Use PathExists= or DirectoryNotEmpty=.
  • Need to react only to future changes? Use PathChanged= or PathModified=.

Multiple Directives in One Unit

You can combine directives. The service fires when any directive matches:

multi-watch.path
[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:

mixed-directives.path
[Path]
PathExists=/var/www/html/deploy.me
DirectoryNotEmpty=/var/www/html/queue/
PathChanged=/var/www/html/.env
Mixing Trigger Behaviors

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 .path and .service unit files.