Skip to content

macOS Local Network Privacy Blocking PM2 from Accessing Remote Workers #24

@jpelaez-23blocks

Description

@jpelaez-23blocks

macOS Local Network Privacy Blocking PM2 from Accessing Remote Workers

Problem Description

When running AI Maestro on macOS 15+ (Sequoia) or macOS 26+ (Tahoe), PM2 processes cannot connect to remote workers on the local network, resulting in EHOSTUNREACH errors.

Symptoms

  • curl http://10.0.0.x:23000/api/sessions works from terminal
  • ✅ Ping and other network tools work fine
  • ❌ PM2-managed Node.js processes get EHOSTUNREACH when trying to connect
  • ❌ Remote worker sessions don't appear in the dashboard
  • ❌ Creating sessions on remote workers fails with "Failed to connect to [host]"

Error Messages

[Sessions] Error fetching from http://10.0.0.18:23000: Error: connect EHOSTUNREACH 10.0.0.18:23000
    at internalConnect (node:net:1098:16)
    ...
  errno: -65,
  code: 'EHOSTUNREACH',
  syscall: 'connect',
  address: '10.0.0.18',
  port: 23000

Root Cause

macOS introduced Local Network Privacy (similar to iOS) starting in macOS 15 (Sequoia). This security feature restricts apps from accessing local network addresses without explicit permission.

Why PM2 is affected:

  • PM2 runs as a launchd agent (~/Library/LaunchAgents)
  • User-level agents are subject to Local Network Privacy restrictions
  • Terminal-launched processes bypass this (Terminal has system permissions)
  • System daemons (/Library/LaunchDaemons) running as root are exempt

Key insight: This is NOT a bug in PM2 or AI Maestro - it's an intentional macOS security feature that blocks user-level background processes from accessing local networks without permission.

Solution

Convert PM2 from a user agent to a system daemon while keeping all PM2 functionality.

Quick Fix (5 minutes)

We provide automated scripts that handle everything:

cd /path/to/ai-maestro

# Step 1: Convert PM2 to system daemon
./scripts/fix-pm2-daemon.sh
# Enter your password when prompted

# Step 2: Kill old PM2 and transition to daemon
./scripts/transition-to-daemon.sh
# Enter your password when prompted

# Step 3: Verify it's working
pm2 list
# You should see your apps running

# Step 4: Test remote connection
curl http://localhost:23000/api/sessions | jq '.sessions | group_by(.hostId)'
# You should see sessions from both local and remote hosts

What the Fix Does

  1. Saves your current PM2 configuration (pm2 save)
  2. Removes user-level PM2 agent (if exists)
  3. Creates system daemon plist at /Library/LaunchDaemons/com.aimaestro.pm2.plist
  4. Installs and loads the daemon (requires sudo)
  5. Kills old PM2 instance and lets system daemon start fresh PM2
  6. Restores your apps (pm2 resurrect)

Result

  • All PM2 commands work normally: pm2 list, pm2 logs, pm2 restart
  • Network access restored: Can connect to remote workers on local network
  • Auto-starts on boot: System daemon launches PM2 automatically
  • Runs as your user: PM2 daemon still runs with your user permissions
  • Same workflow: No changes to how you use PM2

The only difference: PM2 is now controlled by a system daemon instead of a user agent, which exempts it from Local Network Privacy restrictions.

Verification

After applying the fix, verify PM2 is running under the system daemon:

# Check PM2 daemon parent process
ps aux | grep "PM2 v" | grep -v grep
# Look for the PID

ps -p <PM2_PID> -o pid,ppid,user,command
# PPID should be 1 (launchd)

If PPID = 1, PM2 is correctly managed by launchd as a system daemon.

Alternative: Tailscale

If you prefer not to modify PM2, you can use Tailscale to connect workers:

  • Tailscale uses 100.x.x.x addresses (not traditional local network)
  • May bypass Local Network Privacy restrictions (unconfirmed)
  • Requires Tailscale installation on all machines
  • Adds encryption and works across networks

See Network Access Guide for Tailscale setup.

Technical Details

Why System Daemons Are Exempt

From Apple's documentation and developer forums:

"The system does not apply Local Network Privacy to code running as root. If you run an executable as a launchd daemon, it runs as root and local network privacy does not apply."

  • System daemons in /Library/LaunchDaemons are exempt
  • User agents in ~/Library/LaunchAgents are restricted
  • The exemption is based on WHERE the plist lives, not WHO runs the process
  • Even when running as a non-root user via UserName key, system daemons bypass restrictions

Affected macOS Versions

  • ✅ macOS 15.0+ (Sequoia)
  • ✅ macOS 26.0+ (Tahoe)
  • Likely all future macOS versions

macOS 14 and earlier are not affected.

Why curl Works But PM2 Doesn't

Tool Context Local Network Privacy
curl (terminal) Runs from Terminal.app ✅ Allowed (Terminal has permissions)
node (terminal) Runs from Terminal.app ✅ Allowed (inherits Terminal permissions)
PM2 (user agent) Background process ❌ Blocked (needs explicit permission)
PM2 (system daemon) System daemon ✅ Exempt (runs as daemon)

Scripts Reference

All scripts are located in scripts/ directory:

  • scripts/fix-pm2-daemon.sh - Main conversion script (run first)
  • scripts/transition-to-daemon.sh - Complete transition helper (run second)
  • scripts/test-http.js - Test script to verify network access

Related Issues

References

Contributing

If you encounter this issue or have improvements to the fix scripts, please:

  1. Test the provided scripts on your system
  2. Report results (success/failure) with your macOS version
  3. Suggest improvements or alternative solutions
  4. Help update documentation

Labels: bug, documentation, macos, network, distributed
Priority: High (blocks distributed setup on modern macOS)
Status: Documented with automated fix

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingdocumentationImprovements or additions to documentation

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions