A tiny macOS daemon that listens for system wake events and forces a clean NetBird VPN reconnect.
wake → netbird down → short delay → netbird up --enable-lazy-connection → status
- Uses IOKit power notifications (sleep/wake callback).
- Spawns NetBird commands with
posix_spawnp(no shell; resolves via$PATH). - Logs to stderr; when run via Homebrew services, logs are written to Homebrew's log files.
-
Registers a power notification callback with
IORegisterForSystemPower. -
On wake (
kIOMessageSystemHasPoweredOn), runs:netbird down- wait a few seconds (default:
SLEEP_BEFORE_UP_SEC = 5) netbird up --enable-lazy-connectionwithNB_ENABLE_EXPERIMENTAL_LAZY_CONN=true(set internally)netbird status
-
posix_spawnp("netbird", ...)looks upnetbirdin$PATH. -
On startup, the daemon sets a sane default
PATHforlaunchdcontexts if needed:/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin.
- macOS 11+ (Intel or Apple Silicon)
- NetBird CLI installed (e.g.,
brew install netbird) - Xcode Command Line Tools or a recent
clang/cmake
brew tap dmitriimaksimovdevelop/tap
brew install dmitriimaksimovdevelop/tap/macos-netbird-reconnect
brew services start macos-netbird-reconnectStatus & Logs
brew services list | grep -i netbird
# Logs (captured from stderr):
tail -f "$(brew --prefix)/var/log/netbird-reconnectd.log"Stop / Uninstall
brew services stop macos-netbird-reconnect
brew uninstall macos-netbird-reconnectThe Homebrew service definition includes a proper
PATHsoposix_spawnpcan findnetbirdwithout hardcoded paths.
clang -O2 -Wall -Wextra netbird-reconnectd.c -o netbird-reconnectd \
-framework IOKit -framework CoreFoundationcmake -S . -B build
cmake --build build --config Release
# Result: build/netbird-reconnectdEdit in netbird-reconnectd.c if desired:
SLEEP_BEFORE_UP_SEC: seconds to wait after wake beforeup(default5).
You do not need to set the NetBird path. The daemon uses
posix_spawnp("netbird", ...)and relies on$PATH.
./netbird-reconnectd- Logs are written to stderr; you'll see them in your terminal.
- Stop with
Ctrl+C.
Save as ~/Library/LaunchAgents/com.netbird.reconnectd.plist (adjust the absolute path to your binary):
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.netbird.reconnectd</string>
<key>ProgramArguments</key>
<array>
<string>/Users/USERNAME/bin/netbird-reconnectd</string>
</array>
<key>EnvironmentVariables</key>
<dict>
<key>PATH</key>
<string>/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin</string>
</dict>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<false/>
<!-- Optional: route launchd's own stdout/stderr (daemon already logs to stderr) -->
<key>StandardOutPath</key>
<string>/tmp/netbird-reconnectd.launchd.out.log</string>
<key>StandardErrorPath</key>
<string>/tmp/netbird-reconnectd.launchd.err.log</string>
<key>ProcessType</key>
<string>Background</string>
</dict>
</plist>Load / reload:
launchctl bootout gui/$(id -u)/com.netbird.reconnectd 2>/dev/null || true
launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.netbird.reconnectd.plistCheck status and live logs:
launchctl print gui/$(id -u)/com.netbird.reconnectd | head -n 80
log stream --predicate 'process == "netbird-reconnectd"' --style syslog --info-
posix_spawn failed: rc=2 (No such file or directory) for netbird- NetBird CLI not installed or not on the service
$PATH. - Fix:
brew install netbird. With Homebrew services,PATHis set automatically; for manual LaunchAgents, addEnvironmentVariables.PATHin the plist as shown above.
- NetBird CLI not installed or not on the service
-
Service not started / disappears
- Check:
brew services list | grep -i netbird - Foreground run for debugging:
brew services run macos-netbird-reconnect
- Check:
-
Bootstrap failed: 5: Input/output error(manual plist)- Usually an invalid plist or wrong
ProgramArgumentspath. Validate:plutil -lint ~/Library/LaunchAgents/com.netbird.reconnectd.plist.
- Usually an invalid plist or wrong
-
Exec format erroron Apple Silicon- Ensure you built an ARM64 binary (Apple clang does this by default on Apple Silicon). Avoid running Intel-only binaries without Rosetta.
- No shell is invoked; arguments/env are passed directly to
posix_spawnp. - Consider code signing if you distribute the binary.
MIT — see LICENSE.