A lightweight file and directory monitoring utility for FreeBSD and macOS that uses the Kqueue to watch for filesystem changes and execute custom commands in response to events.
- Efficient Monitoring: Uses Kqueue mechanism for low-overhead file system monitoring
- Flexible Configuration: Monitor specific files or entire directory trees
- Event Filtering: Select which event types to monitor (content, structure, metadata)
- Custom Commands: Execute arbitrary commands when events occur
- Dynamic Directory Scanning: Automatically detects and monitors new files and directories as they are created
- Event-Driven Parent Watching: Monitors non-existent paths by watching parent directories and promoting watches as path components are created
- Glob Pattern Support: Dynamic file and directory matching using wildcards (
*,?,[]) with automatic watch promotion when patterns resolve - File and Directory Exclusion: Flexible pattern-based exclusion of files and directories from monitoring using glob syntax
- Hidden File Support: Optional monitoring of hidden files and directories (starting with a dot)
- Resource-Based Event Processing: Tracks filesystem resources and their scanning profiles to detect meaningful changes
- Command Cooldown: Prevent command execution flooding when many events occur rapidly
- Placeholder Substitution: Dynamic command generation based on event details
- Global Variables: Reusable configuration variables with
${VARIABLE}expansion and optional environment injection - Environment Variable Injection: Event context provided to commands when enabled
- Feedback Loop Prevention: Filters out self-generated events and establishes a new baseline after command execution
- Directory Stability Verification: Uses
stat()to recursively verify directory stability before executing commands - Configuration Hot-Reload: Monitors the configuration file and automatically reloads when it changes
- Control Interface: Runtime control via Unix socket for enabling/disabling watches and querying status
- Syslog Integration: Comprehensive logging with configurable verbosity
- Daemon Mode: Run as a background service
- FreeBSD/macOS operating system
- Standard C compiler (cc)
- Make utility (preferably GNU Make)
- Clone the repository or download the source code
- Build the application:
makeGenerate a sample configuration:
make configInstall the application and its configuration file:
make installFor an optimized release build:
make release
make installNote: Due to incompatibilities between macOS and FreeBSD Make utility, it's best to use gmake on FreeBSD.
kqexec [options]
-c, --config=FILE: Configuration file (default: /usr/local/etc/kqexec.conf)-d, --daemon: Run as daemon-l, --loglevel=LEVEL: Set log level (0-7, default: 5)-r, --cooldown=MS: Set command cooldown time in milliseconds (default: 500)-s, --socket-path=PATH: Socket path for control interface (default: /tmp/kqexec.sock)-h, --help: Display help message
When running as a daemon, kqexec provides a control interface for runtime management:
--disable=WATCHES: Temporarily disable specified watches (comma-separated names)--enable=WATCHES: Re-enable previously disabled watches (comma-separated names)--suppress=WATCH:DURATION: Temporarily suppress events for a watch--status: Display current daemon and watch status--list: List all configured watches--reload: Reload configuration from file--socket=PATH: Socket path to connect to (default: /tmp/kqexec.sock)
Examples:
# Disable specific watches
kqexec --disable "Web Content,Log File"
# Enable specific disabled watches
kqexec --enable "Web Content,Log File"
# Suppress a watch for 30 seconds
kqexec --suppress "Log File:30s"
# Check daemon status
kqexec --status
# List all configured watches
kqexec --list
# Reload configuration
kqexec --reloadThe configuration file uses an INI-like format with sections for each watch entry:
[Section Name]
# Comments start with '#'
file = /path/to/file # For monitoring a single file
directory = /path/to/dir # For monitoring a directory
events = EVENT1,EVENT2 # Comma-separated list of events
enabled = true # Whether the watch is initially enabled (default: true)
command = command to execute # Command to run when events occur
environment = false # Whether to set KQ_* environment variables (default: false)
processing_delay = 5000 # Delay in milliseconds before processing events (default: 0)
batch_timeout = 30000 # Batch events and process them when filesystem activity settles (default: 0)
complexity = 2.5 # System responsiveness with 0.1-5.0 range, higher = more cautious (default: 1.0)
log_output = false # Whether to capture and log command output (default: false)
recursive = true # For recursive directory monitoring (default: true)
hidden = false # Whether to monitor hidden files/dirs (default: false)
exclude = *.tmp,build/*,.git # Comma-separated patterns to exclude from monitoringDefine reusable variables in a [Variables] section to reduce configuration duplication:
[Variables]
PROJECT_ROOT = /home/user/myproject
BUILD_SCRIPT = ${PROJECT_ROOT}/scripts/build.sh
ADMIN_EMAIL = admin@company.com
[Source Monitor]
directory = ${PROJECT_ROOT}/src
command = ${BUILD_SCRIPT} && echo "Build complete" | mail ${ADMIN_EMAIL}
environment = true # Makes variables available as KQ_VAR_PROJECT_ROOT, etc.Note: You can instruct kqexec to load the value for a variable from the environment at startup. To do this, define a variable in the [Variables] section where the value is a reference to itself.
[Variables]
# Import API_KEY from the environment
API_KEY = ${API_KEY}
PROJECT_PATH = ${PROJECT_PATH}or
PROJECT_PATH=/home/user/myproject API_KEY=mysecretvalue kqexecWhen kqexec starts, it will look for API_KEY and PROJECT_PATH in the environment and substitute them.
Note: On FreeBSD, when running as a service via rc script, environment variables are not inherited from the global environment. Set them in /etc/rc.conf using the kqexec_env variable:
kqexec_env="HOME=/home/user API_KEY=mysecretvalue PROJECT_PATH=/home/user/myproject"Kqexec supports the following event types that can be specified in the configuration file:
-
STRUCTURE: Monitors directory structure changes- Maps to
NOTE_WRITEandNOTE_EXTENDin kqueue - Most effective for directories, not files
- Triggers when a file's content is modified within a directory
- Triggers for creation, deletion, and renaming operations within a directory
- Example: When items are added to or removed from a directory
- Maps to
-
METADATA: Monitors attribute changes for both files and directories- Maps to
NOTE_ATTRIBandNOTE_LINKin kqueue - Works for both files and directories
- Triggers when permissions, timestamps, or link counts change
- Example: When
chmodorchownis used on a file or directory
- Maps to
-
CONTENT: Monitors changes to file contents- Maps to
NOTE_DELETE,NOTE_RENAME,NOTE_REVOKE, andNOTE_WRITEin kqueue - Triggers when a file's content is modified
- Covers both atomic saves (modern editors) and in-place edits (nano, vi)
- Example: When a text editor saves changes to a file
- Maps to
-
ALL: Monitors all event types (combination of all the above)
Note: When monitoring directories, kqueue may not trigger structural change events if a file’s contents are modified in place. To work around this, set events = STRUCTURE,CONTENT, which will add each individual file in the directory to monitoring.
Commands can include the following placeholders that will be replaced at runtime:
%created: List of created items (newline-separated)%deleted: List of deleted items (newline-separated)%renamed: List of renamed items (format: old -> new, newline-separated)%modified: List of modified files (newline-separated)%p: Full directory path where the event occurred%n: Directory basename where the event occurred%d: Directory path containing the path that triggered the event%b: Base path of the watch from the config%w: Name of the watch from the config%r: Event path relative to the watch path%l: Newline-separated list of changed item basenames%L: Newline-separated list of changed item full paths%q: Size of the file in bytes (recursive for directories)%Q: Human-readable size (e.g., 1.2M, 512K)%t: Time of the event (format: YYYY-MM-DD HH:MM:SS)%u: User who triggered the event%e: Event type which occurred%x: Exclusion patterns as a comma-separated list of single-quoted strings (e.g.,'*.txt','dir3','dir4')
For advanced formatting of array data, you can use template placeholders that apply a format string to each array element. The items in the expanded list are space-separated.
Syntax: %[array_name:template]
Where:
array_nameis the name of the array (see available arrays below)templateis a format string with%splaceholder for each item
Available Arrays:
created: Files/directories that were created (basenames)deleted: Files/directories that were deleted (basenames)renamed: Files/directories that were renamed/movedmodified: Files/directories that were modified (basenames)excluded: Exclusion patterns from theexcludeconfiguration optioncreated_path: Created items with full pathdeleted_path: Deleted items with full pathrenamed_path: Renamed items with full pathmodified_path: Modified items with full path
Examples:
# Multiple --exclude parameters for rsync
command = rsync -av %[excluded:--exclude '%s'] %b/ /backup/
# Expands to: rsync -av --exclude '*.tmp' --exclude 'cache/' /path/ /backup/
# Git add only created and modified files (using basenames)
command = git add %[created:'%s'] %[modified:'%s']
# Expands to: git add 'file1.txt' 'file2.txt' 'modified.txt' 'updated.txt'
# Create a tar archive of all modified files (full paths)
command = tar -czf changes.tar.gz %[modified_path:%s]
# Expands to: tar -czf changes.tar.gz /path/to/file1.txt /path/to/file2.txtTemplate placeholders are more powerful than basic placeholders (like %x, %created) when you need specific formatting for different tools or commands.
In addition to command placeholders, kqexec can optionally set environment variables that provide context about the event. To enable this feature, add environment = true (or env_vars = true) to your watch configuration:
KQ_EVENT_TYPE: Event type (STRUCTURE, CONTENT, METADATA)KQ_TRIGGER_PATH: Full path where the event occurredKQ_WATCH_NAME: Name of the watch from the configurationKQ_WATCH_PATH: Base path being monitoredKQ_RELATIVE_PATH: Event path relative to the watch baseKQ_TRIGGER_DIR: Directory containing the file that triggered the eventKQ_USER_ID: Numeric user ID that caused the eventKQ_USERNAME: Username that caused the event (resolved from user ID)KQ_TIMESTAMP: Timestamp of the event (format: YYYY-MM-DD HH:MM:SS)KQ_CHANGED: Space-separated list of all changesKQ_CREATED: Space-separated list of items createdKQ_DELETED: Space-separated list of items deletedKQ_EXCLUDE: Comma-separated list of single-quoted exclusion patterns (e.g.,'*.txt','dir3','dir4')KQ_RENAMED: Space-separated list of items renamedKQ_MODIFIED: Space-separated list of items modifiedKQ_VAR_*: Global variables from[Variables]section (e.g.,KQ_VAR_PROJECT_ROOT)
These environment variables make commands more powerful and reusable. For example:
#!/bin/bash
case "$KQ_EVENT_TYPE" in
"STRUCTURE")
echo "Directory structure changed in $KQ_WATCH_NAME"
;;
"CONTENT")
echo "File content modified: $KQ_RELATIVE_PATH"
;;
*)
echo "Event $KQ_EVENT_TYPE occurred on $KQ_RELATIVE_PATH"
;;
esac[Configuration Files]
# Monitor system configuration files
directory = /usr/local/etc
events = STRUCTURE,CONTENT
command = logger -p daemon.notice "Configuration changed in %p"
recursive = true
hidden = false
[Log File]
# Monitor a specific log file
file = /var/log/kqexec.log
events = CONTENT
command = echo "Log file %p was modified at %t by user %u (event: %e)" >> /var/log/kqexec_activity.log
processing_delay = 1000
[Web Content]
# Monitor web server content directory recursively
directory = /usr/local/www/data
events = STRUCTURE
command = /usr/local/bin/refresh_cache.sh %p %e
log_output = true
recursive = true
hidden = false
[Script Automation]
# Monitor directory and pass context via environment variables
directory = /home/user/projects
events = STRUCTURE
command = /home/user/scripts/build-deploy.sh
environment = true
log_output = true
recursive = true
[Git Operations]
# Monitor git repository with batch processing for multiple rapid changes
directory = /home/user/projects/myapp
events = STRUCTURE
command = /home/user/scripts/run-tests.sh
batch_timeout = 4000
log_output = true
recursive = true
exclude = .git/*,node_modules/*,*.tmp- Automatic deployment: Monitor a git repository directory and trigger deployment when files change
- Configuration management: Restart services when their configuration files are modified
- Security monitoring: Log all changes to sensitive directories
- Backup verification: Ensure backup jobs complete by monitoring the creation of expected files
- Hidden file monitoring: Track changes in user configuration directories like .config
- Sync files or folders: Keep mirror of documents in multiple locations with rsync
- Git workflow automation: Use
batch_timeoutto handle git operations (clone, merge, checkout) that create many files rapidly - Build system integration: Use
processing_delayfor individual file changes andbatch_timeoutfor bulk operations - Runtime watch management: Enable/disable watches based on system load or maintenance windows using the control interface
To load or unload the daemon:
launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.kqexec.daemon.plist # to load
launchctl bootout gui/$(id -u) ~/Library/LaunchAgents/com.kqexec.daemon.plist # to unloadTo enable at boot add the following line to /etc/rc.conf:
kqexec_enable="YES"Start the service:
service kqexec startNote: make install automatically installs launchd agent or rc script based on your distribution.
kqexec accepts SIGHUP signal for reloading configuration without restarting the application.
On FreeBSD you can run:
service kqexec reloadOr, on macOS:
launchctl kill SIGHUP gui/$(id -u)/com.kqexec.daemonNote: kqexec automatically monitors changes to its configuration file and reloads when modified.
kqexec provides a Unix domain socket-based control interface for runtime management of watches. This allows you to:
- Enable/Disable watches: Temporarily turn watches on or off without restarting the daemon
- Suppress command execution: Block command execution for a specified duration
- Query status: Check which watches are currently active or disabled
- List watches: View all configured watches and their current state
- Reload configuration: Trigger a configuration reload without sending SIGHUP
Watch State Management: Watches can be configured to start in a disabled state using enabled = false in the configuration, then selectively enabled via the control interface as needed.
To prevent flooding when many events occur in rapid succession, kqexec implements a command cooldown mechanism. This rate limit applies per-watch, ensuring the same command won't be executed more frequently than the cooldown period while maintaining accurate baselines when changes occur.
Adjust the cooldown period with the -r option:
kqexec -r 10000 # Set cooldown period to 10000ms (10 seconds)Hidden File Monitoring
kqexec can optionally monitor hidden files and directories (those starting with a dot). This is particularly useful for tracking user configuration files. Enable hidden file monitoring with the include_hidden/hidden option in the configuration:
Commands can span multiple lines using backslash continuation or proper quoting, allowing for complex command structures and shell scripts.
The complexity option (range 0.1-5.0) provides fine-grained control over system responsiveness versus stability. Higher complexity values make the system more cautious and less responsive, while lower values prioritize speed:
- Stability Verification: More checks required before executing commands
- Quiet Period Scaling: Longer wait times before considering directories stable
- Backoff Behavior: More aggressive delays during filesystem instability
- Batch Processing: Higher thresholds for detecting activity gaps
- Depth/Size Sensitivity: Complexity-scaled delays for deep or large directory structures
- Temporary File Window: Increseas period for what is considered temporary or in-progress operation
This allows tuning from very responsive (0.1) for simple workflows to highly cautious (5.0) for complex build systems or intensive I/O operations.
The processing_delay/delay option introduces an initial fixed delay before processing events, useful for scenarios where immediate response isn't required or for bursts of activity which need batching.
The batch_timeout/timeout option defers events during active filesystem operations and processes them as a single operation when activity settles. Rather than a simple timer, it uses activity gap detection - the timeout window resets if events continue arriving, only triggering when there's been no activity for a complexity-determined threshold of the configured duration (by default 50%). This prevents command flooding during chaotic or intensive operations like indexing, builds, or large file transfers.
When log_output/log is enabled, command output is automatically buffered and flushed atomically once the command completes. This provides clean, non-interleaved log output even when multiple commands run concurrently, which is particularly helpful with verbose commands that produce extensive output.
kqexec supports flexible file and directory exclusion patterns using the exclude/ignore configuration option. This feature allows you to prevent specific files or directories from triggering events, which is particularly useful for ignoring temporary files, build artifacts, or version control directories.
Exclusion patterns support glob syntax including:
*: Matches any sequence of characters (except path separators)?: Matches any single character[abc]: Matches any character in the set**: Matches any sequence including path separators (for recursive patterns)
Common exclusion examples:
exclude = *.tmp,*.log # Exclude temporary and log files
exclude = node_modules/* # Exclude Node.js dependencies
exclude = .git,.DS_Store # Exclude version control and system files
exclude = build/**,dist/** # Exclude build directories recursivelyExclusion filtering operates at multiple levels:
- Discovery-time filtering: Excluded directories are not monitored at all, reducing resource usage
- Event-time filtering: Excluded files within monitored directories don't trigger commands
- Glob expansion filtering: Excluded patterns are respected during dynamic watch creation
On FreeBSD, check syslog for messages from kqexec:
grep kqexec /var/log/messagesIf you prefer separate log file, add this lines to /etc/syslog.conf
!kqexec
*.* /var/log/kqexec.logNote: You can add a second-pass filter by changing to *.notice or *.info if log level is set higher.
On macOS:
tail -n 50 ~/Library/Logs/kqexec.log