
Automating Messages
- 13 installs
- 39 repo stars
- Updated January 14, 2026
- spillwavesolutions/automating-mac-apps-plugin
Helps with ai & agent building tasks during AI-assisted development.
About
automating-messages is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted coding.
- automating-messages
- AI & Agent Building
- AI-coding skill
Automating Messages by the numbers
- 13 all-time installs (skills.sh)
- Ranked #11,409 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/spillwavesolutions/automating-mac-apps-plugin --skill automating-messagesAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 13 |
|---|---|
| repo stars | ★ 39 |
| Last updated | January 14, 2026 |
| Repository | spillwavesolutions/automating-mac-apps-plugin ↗ |
What it does
Helps with ai & agent building tasks during AI-assisted development.
Files
Automating Messages (JXA-first with UI/DB fallbacks)
Contents
- Permissions and scope
- Default workflow
- Quick recipe
- Attachments and UI fallback
- Data access and forensics
- Validation Checklist
- When Not to Use
- What to load
Permissions and scope
- Grants needed: Automation + Accessibility; Full Disk Access for any
chat.dbreads. - Keep automation scoped and auditable; avoid unsolicited sends and DB writes.
- Pairs with
automating-mac-appsfor common setup (permissions, osascript invocation, UI scripting basics).
Default workflow (happy path)
1) [ ] Resolve transport: pick serviceType (iMessage or SMS) before targeting a buddy. 2) [ ] Identify recipient: filter buddies by handle (phone/email). Avoid ambiguous names. 3) [ ] Send via app-level send: pass Buddy object to Messages.send(). 4) [ ] Verify window context: activate Messages when mixing with UI steps. 5) [ ] Fallbacks: if send/attachments fail, use UI scripting; for history, use SQL.
Quick recipe (defensive send)
const Messages = Application('Messages');
Messages.includeStandardAdditions = true;
function safeSend(text, handle, svcType = 'iMessage') {
const svc = Messages.services.whose({ serviceType: svcType })[0];
if (!svc) throw new Error(`Service ${svcType} missing`);
const buddy = svc.buddies.whose({ handle })[0];
if (!buddy) throw new Error(`Buddy ${handle} missing on ${svcType}`);
Messages.send(text, { to: buddy });
}- Wrap with
try/catchand log; add small delays when activating UI. - For groups, target an existing chat by GUID or fall back to UI scripting; array sends are unreliable.
Attachments and UI fallback
- Messages lacks a stable JXA attachment API; use clipboard + System Events paste/send.
- Ensure Accessibility permission, bring app forward, paste file, press Enter.
- See
references/ui-scripting-attachments.mdfor the full flow and ObjC pasteboard snippet.
Data access and forensics
Reading messages limitation: The AppleScript/JXA API for Messages is effectively write-only. While send() works reliably, reading messages via chat.messages() or similar methods is broken/unsupported in modern macOS. The only reliable way to read message history is via direct SQLite access to ~/Library/Messages/chat.db.
Security consideration: Reading chat.db requires Full Disk Access permission, which grants broad filesystem access beyond just Messages. This is a significant security trade-off - granting Full Disk Access to scripts or applications exposes all user data. Consider whether reading message history is truly necessary before enabling this permission.
- Use SQL against
chat.dbfor history; JXAchat.messages()is unreliable/non-functional. - Requires: System Settings > Privacy & Security > Full Disk Access for your terminal/script.
- Remember Cocoa epoch conversion (nanoseconds since 2001-01-01); use
sqlite3 -jsonfor structured results. - See
references/database-forensics.mdfor schema notes, typedstream handling, and export tooling.
Example read query (requires Full Disk Access):
sqlite3 ~/Library/Messages/chat.db "SELECT
CASE WHEN m.is_from_me = 1 THEN 'Me' ELSE 'Them' END as sender,
m.text,
datetime(m.date/1000000000 + 978307200, 'unixepoch', 'localtime') as date
FROM message m
JOIN handle h ON m.handle_id = h.rowid
WHERE h.id LIKE '%PHONE_NUMBER%'
ORDER BY m.date DESC LIMIT 10;"Bots and monitoring
- Implement polling daemons with
launchdnow that on-receive handlers are gone. - Track
rowid, query diffs, dispatch actions, and persist state. - See
references/monitoring-daemons.mdfor the polling pattern and plist notes.
Validation Checklist
- [ ] Automation + Accessibility permissions granted
- [ ] Service resolves:
Messages.services.whose({ serviceType: 'iMessage' })[0]returns object - [ ] Buddy lookup works:
svc.buddies.whose({ handle })[0]returns target - [ ] Test send completes without errors
- [ ] Full Disk Access granted if using
chat.dbreads
When Not to Use
- For reading message history without Full Disk Access (AppleScript/JXA cannot read messages)
- For cross-platform messaging (use platform APIs or third-party services)
- For business SMS automation (use Twilio or similar APIs)
- When iMessage/SMS features are not available on the target system
- For bulk messaging (rate limits and security restrictions apply)
- When security policy prohibits Full Disk Access grants (required for any read operations)
What to load
- Control plane and send reliability:
references/control-plane.md - UI scripting + attachments fallback:
references/ui-scripting-attachments.md - SQL/history access:
references/database-forensics.md - Polling bots/launchd:
references/monitoring-daemons.md
Messages JXA control plane
Quick start (1:1 send)
const Messages = Application('Messages');
Messages.includeStandardAdditions = true;
const svc = Messages.services.whose({ serviceType: 'iMessage' })[0]; // or 'SMS'
const buddy = svc.buddies.whose({ handle: '+15551234567' })[0]; // prefer handle over name
try {
Messages.send('Message content', { to: buddy }); // app-level send routes via buddy's service
} catch (err) {
console.log('Send failed', err);
}- JXA objects are specifiers that trigger Apple Events; every property access is a round trip.
handle(phone/email) is most reliable;nameis contact-derived and mutable.- Enable Standard Additions when you need clipboard, alerts, paths, or shell calls.
Service → Buddy resolution
- Multi-protocol app: always pick a
servicefirst (serviceTypeis usuallyiMessageorSMS). - Prefer
whosefilters for deterministic targeting:
const svc = Messages.services.whose({ serviceType: 'iMessage' })[0];
const buddy = svc.buddies.whose({ handle: '+15550001234' })[0];- A
Buddycan exist for each handle per service; avoid ambiguous names.
Reliable send pattern (JXA bridge workaround)
- The Messages JXA bridge misroutes
send; the stable path is app-level `send` + Buddy object. - Never pass a string recipient; it triggers coercion errors (
-1700). - Minimal helper:
function sendMessage(text, handle, svcType = 'iMessage') {
const svc = Messages.services.whose({ serviceType: svcType })[0];
if (!svc) throw new Error(`Service ${svcType} not found`);
const buddy = svc.buddies.whose({ handle })[0];
if (!buddy) throw new Error(`Buddy ${handle} not found on ${svcType}`);
Messages.send(text, { to: buddy });
}- If delivery still fails, switch to UI scripting flow (see
ui-scripting-attachments.md).
Chats and groups
chat.idmaps tochat.dbguid(e.g.,iMessage;+;+1555...for 1:1,iMessage;+;chatXXXXfor groups).- Reading
chat.messages()is slow for long histories; prefer SQL access (seedatabase-forensics.md). - Group creation via
sendto arrays is flaky; use an existing chat by GUID or fall back to UI scripting for multi-recipient sends.
Window management
Messages.activate()brings the app forward (use before UI automation).- Target windows via
Messages.windows.whose({ name: 'John Doe' })when selecting chats visually. - Minimize background runs:
win.minimized = truewhen automation should stay unobtrusive.
Debugging checklist
- Grant Automation + Accessibility permissions; grant Full Disk Access if shelling into
chat.db. - Add short delays after
activate()when switching to UI scripting. - Wrap sends in
try/catchand log errors; the app can drop Apple Event routing mid-run.
chat.db forensics (read-only)
Use SQL for history/analytics; JXA chat iteration is slow and often redacted.
Permissions
- Requires Full Disk Access for
~/Library/Messages/chat.db. - Keep operations read-only; editing DB risks corruption and legal/privacy issues.
Schema snapshot
message: body (textoften NULL on modern macOS),attributedBody(typedstream BLOB),date(ns since 2001-01-01),is_from_me,handle_id.handle:id= phone/email.chat:guidmaps to JXAchat.id.- Joins:
chat_message_joinandchat_handle_join.
Quick pull (last 5 from handle)
const q = `
SELECT text,
datetime((date / 1000000000) + 978307200, 'unixepoch', 'localtime') AS sent_at
FROM message
JOIN handle ON message.handle_id = handle.rowid
WHERE handle.id = '+15551234567'
ORDER BY date DESC LIMIT 5;
`;
const raw = Application.currentApplication().doShellScript(
`sqlite3 -json ~/Library/Messages/chat.db "${q}"`
);
const msgs = JSON.parse(raw);- Use
-jsonto avoid manual parsing. - Convert time via
(date/1e9) + 978307200(Cocoa epoch → Unix).
Typedstream bodies
- Rich/edited messages store content in
attributedBody(NSAttributedString archive). - JXA cannot decode; call out to a helper (Python or Rust) and parse JSON output.
- Popular CLI:
imessage-exporter(/usr/local/bin/imessage-exporter -f json -o /tmp/messages_export), then read JSON in JXA.
Safety and verification
- Always copy the DB before experiments; avoid writes.
- Log SQL errors to a file to diagnose permissions vs schema drift.
- Expect schema changes between macOS releases; guard queries accordingly.
Monitoring + daemon pattern
Use when you need bot-like behavior after AppleScript event handlers were removed.
Polling design
1) Track the latest message.rowid in a state file (e.g., ~/.imessage_bot_last_id). 2) On each run, query chat.db for rowid > last_id. 3) Parse results and dispatch actions (reply, log, trigger script). 4) Persist the new max rowid.
Example shell (invoked from JXA via doShellScript):
STATE=~/.imessage_bot_last_id
[ -f "$STATE" ] || echo 0 > "$STATE"
LAST=$(cat "$STATE")
SQL="SELECT rowid, handle.id, text FROM message JOIN handle ON handle.rowid = message.handle_id WHERE rowid > $LAST ORDER BY rowid;"
RESULTS=$(sqlite3 -json ~/Library/Messages/chat.db "$SQL")
echo "$RESULTS"
NEW_MAX=$(echo "$RESULTS" | jq '.[].rowid' | sort -n | tail -1)
[ -n "$NEW_MAX" ] && echo "$NEW_MAX" > "$STATE"launchd setup (preferred over while-true loops)
- Create
~/Library/LaunchAgents/com.user.messagebot.plistwithProgramArgumentspointing to your script andStartInterval(e.g., 10 seconds). - Load/unload with
launchctl bootstrap gui/$UID ...andlaunchctl bootout gui/$UID .... - Keep scripts idempotent;
launchdmay overlap if runs take long.
Hardening
- Verify Full Disk Access for the agent binary/script.
- Add structured logging (JSON) for postmortem analysis.
- Rate-limit actions to avoid UI thrash when many messages arrive.
UI scripting + attachments
Use when JXA send fails or you must deliver attachments (Messages has no JXA attachment API).
Preconditions
- System Settings → Privacy & Security → Accessibility: allow the editor/terminal.
- Grant Automation permissions to send events to Messages and System Events.
- Best-effort clipboard requires Standard Additions; ObjC clipboard improves reliability.
Copy–Paste–Send flow
1) Prep: Messages.activate(); delay(0.5); (give the window manager time). 2) Select chat: ensure the right conversation is frontmost (select via JXA or UI). 3) Load clipboard:
- Basic:
Messages.theClipboard = Path('/path/to/file'); - Robust (ObjC pasteboard):
ObjC.import('AppKit');
const pb = $.NSPasteboard.generalPasteboard;
pb.clearContents();
pb.writeObjects([$.NSURL.fileURLWithPath('/path/to/file')]);4) Inject:
const SE = Application('System Events');
const proc = SE.processes['Messages'];
SE.keystroke('v', { using: 'command down' }); // paste file or rich content
delay(0.2);
SE.keyCode(36); // Enter to send5) Cleanup: optional restore of the previously active app.
Robustness patterns
- Replace fixed delays with wait loops (e.g., while !proc.windows.length()) when feasible.
- Guard every UI call with
try/catchand log to a temp file for forensics. - Keep clipboard scoped: store and restore prior contents if user context matters.
- If paste silently fails, verify Accessibility permission and that the chat input is focused.