Making iMessage Reliable with OpenClaw: 3 Problems and How We Fixed Them
Running OpenClaw's iMessage channel 24/7 on a Mac mini revealed three reliability issues — FSEvents coalescing, attachment path sandboxing, and TCC permission resets. Here's what went wrong and how we fixed each one.
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
import { execSync } from 'child_process';
import { utimesSync } from 'fs';
import { homedir } from 'os';
import { join } from 'path';
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);
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>
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"
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))
In our logs, this happened on Feb 13 and Feb 24, 2026 — both times correlating with macOS updates.
Fix: Manual, unfortunately.
-
Check the gateway error log:
grep "permissionDenied" ~/.openclaw/logs/gateway.err.log | tail -5 -
If you see
code: 23, go to: System Settings → Privacy & Security → Full Disk AccessMake 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. -
Verify:
/opt/homebrew/bin/imsg chats --limit 1 # Should return your most recent chat, not an error -
Restart:
openclaw gateway restart
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
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
-
“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.
-
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.
-
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. -
Log everything. The poller logs every
touchit performs. The gateway logs every permission error. Without these, we’d still be debugging “why didn’t my message go through?”