Keeps the GUI app of the LuLu firewall (by Objective-See) alive so new-connection alerts are never silently missed.
LuLu's network extension keeps enforcing existing rules even when the GUI app is closed — the extension itself runs independently. However, alerts for new connections are displayed by the GUI app. When the GUI silently quits (which happens occasionally), you never get the chance to allow or deny new connections. This watchdog treats that symptom.
- Checks every 30 seconds — a LaunchAgent fires one zsh invocation and one
pgrep, taking a few milliseconds of CPU and zero resident memory between ticks. - Relaunches LuLu hidden in the background — uses
open -gj -a LuLu.appso the app appears without stealing focus. - Confirms the relaunch within 10 seconds — polls until the process is visible, then logs the PID.
- Logs open failures with their exit code — the next 30-second tick is the automatic retry.
- Self-disables after 10 minutes of continuous LuLu absence — it takes 20 consecutive "missing" ticks to trigger
launchctl bootout, so a LuLu update (which can briefly replace the.appbundle) does not disable the watchdog. - Auto-re-enables at next login —
RunAtLoad: truein the plist restarts the agent after each login. - Rotates its log at 256 KB, keeping 3 rotated files.
- Robust process detection — an anchored
pgrep -fpattern that tolerates CLI arguments, plus apgrep -xfallback for app-translocation paths, both scoped to the current user (-u $UID) so another user's LuLu process under fast user switching does not mask the absence. - macOS notifications with sounds — localized to Polish or English based on the system locale; at most one notification per outage episode (a freshness marker both prevents spam on repeated failures and silences the login race where the watchdog's first tick fires before LuLu's own login item).
| Event | English | Polski | Sound |
|---|---|---|---|
| Relaunched | LuLu had quit — relaunched (PID …) | LuLu zamknęło się — uruchomiono ponownie (PID …) | Glass |
open failed |
LuLu quit and the relaunch failed (open exit …) | LuLu zamknęło się, a ponowne uruchomienie nie powiodło się (open: kod …) | Basso |
| Relaunch unconfirmed | LuLu quit and the relaunch was not confirmed — check it manually | LuLu zamknęło się, a ponowne uruchomienie nie zostało potwierdzone — sprawdź ręcznie | Basso |
| Watchdog self-disabled | LuLu app is still missing — watchdog disabled until next login | Aplikacji LuLu wciąż brak — watchdog wyłączony do następnego logowania | Basso |
Notifications are posted via osascript — if they do not appear, allow notifications from Script Editor in System Settings → Notifications. To force a language for the agent, add to the plist and re-run ./install.sh:
<key>EnvironmentVariables</key>
<dict>
<key>LULU_WATCHDOG_LANG</key>
<string>en</string> <!-- or "pl" -->
</dict>Why not point launchd KeepAlive directly at LuLu's binary?
LuLu registers its own login item. Having a second KeepAlive agent fight that item risks spawning a second instance and interfering with LuLu's own lifecycle management.
Why 30-second polling instead of a resident NSWorkspace notification observer?
Polling costs zero resident memory between ticks. A false "not running" verdict is harmless: open -a on an already-running app does not launch a second instance — it is idempotent.
No sudo required — this is a per-user LaunchAgent.
git clone https://github.com/adriank1410/lulu-watchdog.git
cd lulu-watchdog
./install.shThe installer and uninstaller auto-detect English or Polish from the system locale (AppleLocale). Override with:
LULU_WATCHDOG_LANG=en ./install.sh # force English
LULU_WATCHDOG_LANG=pl ./install.sh # force Polish./uninstall.shLog files remain in ~/Library/Logs/LuLuWatchdog.log* after uninstall.
# Watch the live log
tail -f ~/Library/Logs/LuLuWatchdog.log
# Check agent status
launchctl print gui/$UID/com.local.lulu-watchdog
# Apply edits to the watchdog script
./install.shImportant: To intentionally quit LuLu, stop the watchdog first — otherwise it resurrects LuLu within 30 seconds:
launchctl bootout gui/$UID/com.local.lulu-watchdog
Edit the constants at the top of lulu-watchdog.zsh, then re-run ./install.sh to apply.
| Constant | Default | Description |
|---|---|---|
app_path |
/Applications/LuLu.app |
Path to the LuLu application bundle |
lulu_executable |
$app_path/Contents/MacOS/LuLu |
Expected executable inside the bundle |
log_file |
~/Library/Logs/LuLuWatchdog.log |
Log file path |
state_dir |
~/Library/Application Support/LuLuWatchdog |
State directory (miss counter) |
agent_label |
com.local.lulu-watchdog |
LaunchAgent label |
max_log_bytes |
262144 (256 KB) |
Log size that triggers rotation |
max_rotated_logs |
3 |
Number of rotated log files to keep |
max_app_missing_checks |
20 |
Consecutive "missing" ticks before self-disable (20 × 30 s = 10 min) |
launch_confirm_timeout |
10 |
Seconds to wait for relaunch confirmation |
notify_enabled |
1 |
Set to 0 to disable macOS notifications |
notify_fresh_seconds |
45 |
Notify only if LuLu was seen running this recently (anti-spam / login-race guard); keep between one and two ticks |
StartInterval |
30 |
Seconds between ticks (set in the plist, not the script) |
| Repo file | Installed to |
|---|---|
lulu-watchdog.zsh |
~/Library/Application Support/LuLuWatchdog/lulu-watchdog |
com.local.lulu-watchdog.plist |
~/Library/LaunchAgents/com.local.lulu-watchdog.plist |
| (generated at runtime) | ~/Library/Logs/LuLuWatchdog.log |
The script is installed without the .zsh extension and executed directly by launchd (not as zsh script.zsh) — this way System Settings → General → Login Items lists the agent as lulu-watchdog instead of an anonymous zsh.
The test suite runs sandboxed copies of the script with substituted paths. It requires neither LuLu installed nor running, and never touches the real LaunchAgent.
zsh tests/test_watchdog.zsh- macOS with LuLu installed in
/Applications