Diagnose and fix iMessage not responding, not receiving, or not replying on macOS Instar agents. Covers the stale WAL-hardlink root cause, the deadlocked IMDPersistenceAgent, and the tmux/terminal Automation (TCC) grant that silently breaks sending after a Homebrew upgrade. Trigger words - imessage not working, imessage not responding, not getting imessage, imessage stopped, imessage broken, no reply, chat.db, apple events not authorized, -1743.
Install
npx skillscat add rolandcanyon-cmd/home-consciousness/imessage-doctor Install via the SkillsCat registry.
imessage-doctor — diagnose & repair the macOS iMessage path
Use this when iMessage isn't responding, isn't receiving, or "worked yesterday, broke after an
update/reboot." It encodes the real architecture so you don't re-diagnose from scratch.
Set these once per session:
AGENT="$(cd "$(dirname "${BASH_SOURCE:-$PWD}")" && pwd)" # or just: AGENT=/path/to/.instar/agents/<Name>
MSG="$HOME/Library/Messages"
AUTH=$(python3 -c "import json;print(json.load(open('$AGENT/.instar/config.json')).get('authToken',''))")Split the problem in two FIRST
iMessage has two independent halves, and they fail for completely different reasons:
| Half | Mechanism | Fails because |
|---|---|---|
| RECEIVE | daemon reads chat.db via hardlinks |
stale -wal hardlink (#1) or frozen DB (#2) |
| SEND | imsg/AppleScript → Messages.app |
Automation (AppleEvents) TCC grant (#3) |
Do not skip this triage. "Roland isn't responding" is usually SEND, not RECEIVE — the message
arrives, a session spawns, an ack goes out, and then the reply silently fails. Check both:
# RECEIVE healthy? live and private DBs must agree
echo -n "live : "; sqlite3 "$MSG/chat.db" "select datetime(max(date)/1000000000+978307200,'unixepoch','localtime') from message;"
echo -n "priv : "; sqlite3 "$AGENT/.instar/imessage/chat.db" "select datetime(max(date)/1000000000+978307200,'unixepoch','localtime') from message;"
# SEND healthy? this is the decisive one-liner
imsg send --to "+1XXXXXXXXXX" --text "probe" ; echo "exit=$?"An exit of 0 from your interactive shell does not prove send works for the agent —
see #3, the grant is per-ancestor-process.
The architecture (explains every RECEIVE failure)
The node server daemon deliberately does NOT have Full Disk Access (FDA). macOS protects~/Library/Messages/ behind TCC, and node's FDA grant is lost on every node version bump — so
relying on it is fragile. Instead the daemon reads through hardlinks in a non-protected dir:
~/Library/Messages/chat.db ──hardlink──▶ .instar/imessage/chat.db
~/Library/Messages/chat.db-wal ──hardlink──▶ .instar/imessage/chat.db-wal ← the critical one
~/Library/Messages/chat.db-shm ──hardlink──▶ .instar/imessage/chat.db-shmdbPath in config.json points at .instar/imessage/chat.db — correct and intentional, not a
hack. Do NOT "fix" it by pointing at the live path; node lacks FDA and fails withunable to open database file.
Why the -wal file is everything: SQLite (Messages.app) writes new messages to the -wal
sidecar; they only land in chat.db on an irregular checkpoint. A reader sees new messages only
if it reads the live -wal. The hardlink shares the live inode — UNTIL macOS Messages
recreates the -wal with a new inode (every machine reboot / Messages restart). Then the
private hardlink points at a dead inode and iMessage silently stops seeing anything new.
A plain server restart does NOT recreate the WAL, so it does not break the links. Only a machine
reboot / Messages restart does.
Two FDA-holding processes maintain the hardlinks:
instar-attachments-sync(Go, LaunchAgentai.instar.AttachmentsWatcher) — re-links
chat.db/wal/shm every 2s, so reboots self-heal.IMessageAdapter.ensureChatDbHardlink()— startup best-effort; needs FDA on node (usually absent).
Fast diagnosis
# 1) adapter connected?
curl -s -H "Authorization: Bearer $AUTH" http://localhost:4040/imessage/status # want {"state":"connected"}
# 2) THE key RECEIVE check — priv older than live ⇒ stale WAL hardlink
# (see the two sqlite3 lines above)
# 3) confirm via inodes (the wal one differs when broken)
for n in chat.db chat.db-wal chat.db-shm; do
printf "%-14s live=%s priv=%s\n" "$n" "$(stat -f %i "$MSG/$n" 2>/dev/null)" "$(stat -f %i "$AGENT/.instar/imessage/$n" 2>/dev/null)"
done
# 4) watcher healthy? "operation not permitted" here = FDA problem
tail -5 "$AGENT/.instar/logs/attachments-watcher.log"
# 5) did a session spawn and did it reply?
grep -i 'imessage' "$AGENT/logs/server.log" | tail -30Step 5 is the one people skip. [imessage→session] Injecting… followed by Sent immediate ack
and then nothing is the signature of a SEND failure (#3), not a receive failure.
Root cause #1 — stale -wal hardlink
Symptom: priv timestamp older than live; inodes differ. Agent sees only stale checkpointed
data and silently stops responding to anything new.
Fix A — relink now (needs a shell WITH Full Disk Access):
bash "$AGENT/.claude/scripts/imessage-relink-chatdb.sh" --restartIf it prints this shell lacks Full Disk Access, FDA is the real problem → Fix C.
Fix B — durable self-heal: ensure the instar-attachments-sync watcher is running; it re-links
every 2s. After ANY rebuild of that binary macOS flips its FDA grant to DENIED
(operation not permitted in the log) → Fix C.
Fix C — re-grant Full Disk Access (GUI, user-only; TCC is SIP-protected):
System Settings → Privacy & Security → Full Disk Access → find instar-attachments-sync,
toggle OFF then ON (or remove with – and re-add with +), then:
launchctl kickstart -k gui/$(id -u)/ai.instar.AttachmentsWatcherVerify (read-only): auth_value 2 = granted, 0 = denied.
sqlite3 "/Library/Application Support/com.apple.TCC/TCC.db" \
"select client,auth_value from access where service='kTCCServiceSystemPolicyAllFiles' and client like '%attachments-sync%';"Root cause #2 — stale duplicate IMDPersistenceAgent deadlocks the store
Symptom: Messages.app is open and signed in, the conversation shows then clears, andchat.db is completely frozen — the -wal is byte-for-byte identical for tens of minutes. No
new rows, not even for messages you SEND. Hardlinks are perfect and the adapter is connected.
Looks like iMessage is signed out; it isn't.
Cause: a macOS logout/login leaves orphaned imagent + IMDPersistenceAgent processes. A
stale IMDPersistenceAgent holds chat.db open without writing, so the live agent can never
persist. The WAL freeze timestamp matches the stale agent's start time exactly — that's the tell.
ps -o pid,lstart,comm -p $(pgrep -f "imagent|IMDPersistenceAgent" | tr '\n' ',' | sed 's/,$//')
lsof ~/Library/Messages/chat.db-wal | grep IMDPers
stat -f "%Sm %z" -t "%H:%M:%S" ~/Library/Messages/chat.db-wal # run twice, 30s apartFix:
osascript -e 'tell application "Messages" to quit'; sleep 2
pkill -9 -f "IMDPersistenceAgent"; pkill -9 -f "IMCore.framework/imagent"
sleep 5; open -a Messages
# verify: WAL size changes within ~20s and new rows appearA logout/login does NOT clear these orphans — only a real reboot guarantees a clean set.
Always check sysctl -n kern.boottime; "I restarted" often means Messages or a logout, not the machine.
Root cause #3 — Automation (AppleEvents) TCC grant lost after a Homebrew upgrade
This one breaks SEND only, and it is invisible in every receive-side check.
Symptom: messages arrive fine, a session spawns, the ... ack goes out — and the real reply
never lands. The session transcript shows:
appleScriptFailure("execution error: Not authorized to send Apple events to Messages. (-1743)")Meanwhile imsg send from your own terminal works perfectly. That contradiction is the tell.
Cause: macOS attributes AppleEvents permission to the responsible ancestor process, not toimsg. For agent sessions that ancestor is tmux. The grant is keyed to the tmux binary's
exact absolute path, e.g. /opt/homebrew/Cellar/tmux/3.6a/bin/tmux. Abrew upgrade tmux installs a new Cellar path (…/3.7c/bin/tmux), which TCC treats as a brand-new
client with no grant — and a headless LaunchAgent context cannot show a consent prompt, so it
records a denial (auth_value 0) that never re-prompts. Sending dies silently and permanently.
The same applies to any terminal/multiplexer upgrade in the ancestry.
Diagnose:
# which tmux is actually running the agent's sessions?
for p in $(pgrep -x tmux); do lsof -p $p 2>/dev/null | awk '$4=="txt"{print $NF; exit}'; done
# the grants — 2 = allowed, 0 = DENIED
sqlite3 "$HOME/Library/Application Support/com.apple.TCC/TCC.db" \
"select client, indirect_object_identifier, auth_value from access where service='kTCCServiceAppleEvents';"A row for the old tmux version with 2 plus a row for the current version with 0 is a
positive identification. Cross-check the timing: ls -la /opt/homebrew/Cellar/tmux/ shows the
upgrade date, and it will match the day iMessage replies stopped.
Prove it in 30 seconds — same command, same user, only the ancestor differs:
TM=/opt/homebrew/bin/tmux
$TM -L probe new-session -d -s p
$TM -L probe send-keys -t p 'imsg send --to "+1XXXXXXXXXX" --text "probe" > /tmp/p.out 2>&1' Enter
sleep 6; cat /tmp/p.out; $TM -L probe kill-serverWorks in your shell, -1743 under tmux ⇒ confirmed.
Fix (preferred, GUI — durable and surgical):
System Settings → Privacy & Security → Automation → find tmux → enable Messages.
Fix (agent-side, no GUI): flip the single denied row. Back up first.
TCC="$HOME/Library/Application Support/com.apple.TCC/TCC.db"
cp "$TCC" "/tmp/TCC.db.bak-$(date +%s)"
sqlite3 "$TCC" "update access set auth_value=2
where service='kTCCServiceAppleEvents'
and client='/opt/homebrew/Cellar/tmux/<VERSION>/bin/tmux'
and indirect_object_identifier='com.apple.MobileSMS';"⚠️ A running tmux server caches its TCC verdict. After the grant change, a fresh tmux server
sends fine while the existing one still returns -1743. You MUST bounce it:
tmux kill-server # recycles agent sessions; they respawn
launchctl kickstart -k gui/$(id -u)/ai.instar.<AgentName>Then re-probe. If the probe passes but the agent still doesn't reply, the sessions predate the
bounce — restart them.
This recurs on every tmux upgrade. Treat a silent send failure right after anybrew upgrade as this root cause until disproven, and consider pinning tmux
(brew pin tmux) so the grant survives.
Also-check (links fine, still no reply)
- Server up?
curl -s http://localhost:4040/health. After a reboot the server only starts on
GUI login. If it crash-loops with[single-instance] lock held by FOREIGN host, clear the stale.instar/local/server-instance.lockwhen its pid is dead, then restart. - Session spawning?
grep 'imessage→session' "$AGENT/logs/server.log" | tail database disk image is malformed→ transient WAL corruption; usually clears on reconnect.- Verify sends actually landed, don't trust exit codes:
sqlite3 "$MSG/chat.db" "select datetime(date/1000000000+978307200,'unixepoch','localtime'),
is_from_me, is_sent, is_delivered, error from message order by date desc limit 5;"Outgoing rows show empty text because modern macOS stores the body in attributedBody — that is
normal and not a fault. Judge by is_sent / is_delivered / error.
Triage order
- Server up? →
/health. chat.dbFROZEN? (WAL identical across 30s) ⇒ #2, staleIMDPersistenceAgent.- DB moving but agent sees stale data? ⇒ #1, stale
-walhardlink. - Receive fine, ack sent, no reply? ⇒ #3, Automation TCC grant after a tmux upgrade.
- Watcher log
operation not permitted⇒ re-grant Full Disk Access (Fix C).
One-line summary
iMessage silent ⇒ check the server is up, then decide whether it's RECEIVE (frozen DB ⇒ staleIMDPersistenceAgent; or moving-but-unseen ⇒ stale -wal hardlink) or SEND (ack goes out but
no reply ⇒ tmux lost its Automation grant to Messages after a Homebrew upgrade).