DEV Community

Qiushi
Qiushi

Posted on • Originally published at claw-stack.com

Making iMessage Reliable with OpenClaw: 3 Problems and How We Fixed Them

OpenClaw can use iMessage as a communication channel — you text your AI agent, it texts you back. Sounds simple, but running it 24/7 on a Mac mini revealed three reliability issues that took weeks to fully diagnose. Here's what went wrong and how we fixed each one.

The Setup

OpenClaw's iMessage plugin works by watching ~/Library/Messages/chat.db via filesystem events (FSEvents). When a new message arrives, macOS writes to chat.db, the watcher detects the change, and the gateway processes the message.

In theory, this is instant. In practice, it breaks in three distinct ways.

Problem 1: Messages Delayed Up to 5 Minutes When Idle

Symptom: You send a message, it shows "Delivered" on your phone, but the agent doesn't respond for 3-5 minutes. Then suddenly it processes everything at once.

Root Cause: macOS power management coalesces FSEvents for background processes. Even with ProcessType=Interactive in the LaunchAgent plist and caffeinate running, the kernel still batches vnode events on chat.db during low-activity periods. The imsg rpc subprocess watches the file, but macOS decides "this process hasn't been active, let's batch up those file notifications."

Why It's Tricky: The message is already in chat.db — it's the notification that's delayed, not the message itself. So everything works perfectly during active use, but fails silently when the machine is idle.

Fix: A polling script that checks chat.db every 15 seconds and touches the file when new rows appear, generating a fresh FSEvent:

#!/usr/bin/env node
// imsg-poller.mjs — Polls chat.db for new messages and wakes FSEvents watcher


const CHATDB = join(homedir(), 'Library/Messages/chat.db');
const INTERVAL = 15000; // 15 seconds

function getMaxRowid() {
  try {
    return execSync(
      `/usr/bin/sqlite3 "${CHATDB}" "SELECT MAX(ROWID) FROM message;"`,
      { timeout: 5000, encoding: 'utf8' }
    ).trim() || '0';
  } catch { return '0'; }
}

let lastRowid = getMaxRowid();
if (lastRowid === '0') {
  console.error('ERROR: Cannot read chat.db — check Full Disk Access');
  process.exit(1);
}

console.log(`imsg-poller started. ROWID: ${lastRowid}, interval: ${INTERVAL}ms`);

setInterval(() => {
  const current = getMaxRowid();
  if (current !== '0' && current !== lastRowid) {
    console.log(`New message (ROWID ${lastRowid} -> ${current}), touching chat.db`);
    try {
      const now = new Date();
      utimesSync(CHATDB, now, now);
    } catch (e) {
      console.error(`touch failed: ${e.message}`);
    }
    lastRowid = current;
  }
}, INTERVAL);
Enter fullscreen mode Exit fullscreen mode

Why Node.js instead of bash? We tried a bash version first, but launchd-spawned /bin/bash processes don't inherit Full Disk Access (TCC). The stat command works, but sqlite3 gets "authorization denied". Using /opt/homebrew/bin/node works because it inherits FDA from the same TCC grant as the gateway.

Deployment: Run as a LaunchAgent with KeepAlive: true:

<?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>ai.openclaw.imsg-poller</string>
  <key>ProgramArguments</key>
  <array>
    <string>/opt/homebrew/bin/node</string>
    <string>/path/to/imsg-poller.mjs</string>
  </array>
  <key>RunAtLoad</key><true/>
  <key>KeepAlive</key><true/>
  <key>EnvironmentVariables</key>
  <dict>
    <key>HOME</key><string>/Users/youruser</string>
  </dict>
  <key>ThrottleInterval</key><integer>10</integer>
</dict>
</plist>
Enter fullscreen mode Exit fullscreen mode

Survives OpenClaw updates? Yes — it's a standalone launchd job.

Problem 2: Images Sent via iMessage Fail with "Path Not Allowed"

Symptom: The agent tries to send an image that was received via iMessage, but gets "Local media path is not under an allowed directory." The image exists at ~/Library/Messages/Attachments/... but OpenClaw's media sandboxing blocks it.

Root Cause: OpenClaw's buildMediaLocalRoots() function defines which directories are allowed for media file access. It includes the workspace, temp directories, and sandboxes — but not ~/Library/Messages/Attachments/. When the agent tries to forward or process an image received via iMessage, the path is rejected.

Fix: A patch script that adds the Messages attachment directory to the allowed roots:

#!/usr/bin/env bash
# patch-imessage-attachments.sh
# Adds ~/Library/Messages/Attachments to allowed media roots
# Re-run after every `npm update -g openclaw`

DIST="/opt/homebrew/lib/node_modules/openclaw/dist"

patched=0
for f in "$DIST"/ir-*.js; do
  [ -f "$f" ] || continue
  if grep -q "buildMediaLocalRoots" "$f" && \
     ! grep -q "Messages/Attachments" "$f"; then
    sed -i '' 's|path.join(resolvedStateDir, "sandboxes")|path.join(resolvedStateDir, "sandboxes"),\n\t\tpath.join(os.homedir(), "Library/Messages/Attachments")|' "$f"
    echo "Patched: $(basename $f)"
    patched=$((patched + 1))
  fi
done

echo "Done. Patched: $patched files"
echo "Run: openclaw gateway restart"
Enter fullscreen mode Exit fullscreen mode

Survives OpenClaw updates? No — the compiled JS files are overwritten. You must re-run this after every update.

Problem 3: macOS Updates Silently Revoke Full Disk Access

Symptom: iMessage stops working entirely. No messages received, no errors in the gateway log that make sense. The agent appears online but is deaf.

Root Cause: macOS system updates (and sometimes minor security patches) can reset TCC (Transparency, Consent, and Control) permissions. When this happens, the imsg binary loses Full Disk Access, which means it can't read ~/Library/Messages/chat.db. The gateway logs show:

permissionDenied(path: "~/Library/Messages/chat.db",
  underlying: authorization denied (code: 23))
Enter fullscreen mode Exit fullscreen mode

In our logs, this happened on Feb 13 and Feb 24, 2026 — both times correlating with macOS updates.

Fix: Manual, unfortunately.

  1. Check the gateway error log:
   grep "permissionDenied" ~/.openclaw/logs/gateway.err.log | tail -5
Enter fullscreen mode Exit fullscreen mode
  1. If you see code: 23, go to: System Settings → Privacy & Security → Full Disk Access

Make sure imsg (or Terminal / iTerm, whichever runs your gateway) has FDA enabled. Toggle it off and on if it looks correct but isn't working.

  1. Verify:
   /opt/homebrew/bin/imsg chats --limit 1
   # Should return your most recent chat, not an error
Enter fullscreen mode Exit fullscreen mode
  1. Restart:
   openclaw gateway restart
Enter fullscreen mode Exit fullscreen mode

Survives OpenClaw updates? Yes — TCC permissions are system-level. But macOS updates can reset them.

The Post-Update Checklist

Every time you run npm update -g openclaw, do this:

# 1. Re-apply patches (overwritten by update)
bash ~/.openclaw/autopatch/patch-imessage-attachments.sh

# 2. Restart gateway
openclaw gateway restart

# 3. Verify iMessage works
/opt/homebrew/bin/imsg chats --limit 1
Enter fullscreen mode Exit fullscreen mode

After macOS updates, also check Full Disk Access permissions.

Should OpenClaw Fix These Upstream?

Problem 1 (FSEvents coalescing) is a macOS kernel behavior — hard to fix in OpenClaw itself. The poller is the right workaround. OpenClaw could ship it as an optional component.

Problem 2 (attachment path) is a clear bug/oversight. ~/Library/Messages/Attachments/ should be in the default allowed roots when the iMessage plugin is enabled. This is a one-line fix upstream.

Problem 3 (TCC reset) is Apple's problem. Nothing OpenClaw can do except maybe detect it and log a clearer error message.

Lessons Learned

  1. "Works on my machine" isn't enough for always-on agents. These bugs only appear after days of continuous operation or after system updates. You need to run your agent 24/7 for weeks to find them.

  2. macOS is not designed for headless servers. Power management, TCC, FSEvents coalescing — they all assume a human is sitting in front of the screen. Running an AI agent on a Mac mini requires fighting the OS at every level.

  3. Keep a patch directory. We maintain ~/.openclaw/autopatch/ with scripts and a README documenting every patch. When an update lands, we run them all. It's not elegant, but it's reliable.

  4. Log everything. The poller logs every touch it performs. The gateway logs every permission error. Without these, we'd still be debugging "why didn't my message go through?"

This article was originally published on claw-stack.com. We're building an open-source AI agent runtime — check out the docs or GitHub.

Top comments (0)