Frontier Agent Interaction on iMessage: Tech Overview
Preface
At Photon, we keep asking ourselves a simple question: what should AI and agents actually look like in the future?
Will we really open a browser, type in a URL, and use agents as if they were just another SaaS tool? Will we download yet another app just to “chat with an AI boyfriend or girlfriend”? This feels unexciting and it certainly does not match the futures we see in sci-fi scenes.
We believe that in the world we are heading toward, AI should not appear as a “feature” or a “tool.” It should feel like a type form of life, deeply woven into our social structures. Our kids will not be surprised when they see AI as we were. They will not treat it as a cold, external program as we treat our macs and phones. They will look at AI the same way they look at friends and classmates.
In that world, AI becomes a first‑class citizen in our society.
With that in mind, we started asking what we can build today that moves us in that direction. One answer we kept coming back to was iMessage.
In the United States, almost everyone uses iMessage every day. Millions of messages flow through it constantly. It might be the most natural and native interaction surface for agents in this era: the agent shows up in your conversation list like a friend, and even joins your group chats.
So we decided to turn this idea into reality and build infrastructure that lets AI exist in a truly “human” way inside iMessage.
That is how we arrived at imessage-kit - an open‑source TypeScript SDK for controlling iMessage. It lets developers send, receive, and orchestrate iMessage messages through code.
Along the way, we had to work through a lot of technical constraints and ended up re‑imagining how AI can communicate with people.
This blog focuses on the technical side of that journey: how iMessage works under the hood, what it takes to build a reliable SDK around it, and how that unlocks new interaction patterns for agents. We will save more speculative interaction design and UX experiments for future posts.
1. Understanding iMessage’s Data Architecture
1.1 Where the database lives and how it is structured
All iMessage messages are stored in a single SQLite database located here:
This database has existed since the very first versions of iMessage. If you have been using iMessage for years, your chat.db file may already be hundreds of megabytes, or even over 1GB.
Core tables:
These tables together describe who is talking to whom, what was sent, and which files are attached.
1.2 Timestamps: the Mac epoch
The timestamps in this database are not Unix timestamps. For example:
If you do something like new Date(408978598) directly, the result will be wrong.
Key point: macOS uses its own epoch starting from 2001‑01‑01, instead of Unix’s 1970‑01‑01.
Here is the correct conversion pattern:
This “Mac epoch” style is standard across macOS and iOS (Core Data timestamps). Once you get used to it, it becomes straightforward.
1.3 Message body encoding: NSAttributedString
Text in iMessage is stored across two fields:
message.text: plain textmessage.attributedBody: rich text, stored as a binary plist representing anNSAttributedString
In imessage-kit, we use two strategies to parse attributedBody.
Strategy 1: Direct string extraction (fast but a bit rough)
We use a regex to pull out human‑readable text from the binary, and then filter out plist keywords such as NSAttributedString, NSDictionary, and so on.
Strategy 2: Use plutil (precise but slower)
macOS ships with a command‑line tool plutil that converts binary plists into XML. From there, we can parse out the contents of <string> tags.
Each approach has tradeoffs:
Strategy 1 is fast but may capture some noisy or broken strings.
Strategy 2 is very accurate but requires temporary files and shelling out to a system command.
In practice, imessage-kit uses a hybrid approach: try Strategy 1 first, and if it fails, fall back to Strategy 2. This gives a good balance between speed and correctness.
2. Working Around macOS Security
2.1 Full Disk Access
Starting from macOS Mojave (10.14), Apple tightened privacy controls. The ~/Library/Messages folder is now protected. Programs without explicit permission cannot access it.
Typical error message when you try to access:
How to fix it:
Open System Settings → Privacy & Security → Full Disk Access.
Click the “+” button and add your terminal and/or IDE (Terminal, iTerm, VS Code, Cursor, etc.).
Restart those apps. This is critical. Permissions do not take effect until you restart.
We recommend adding both your main editor and terminal to avoid permission inconsistency in different environments.
2.2 SQLite WAL mode behavior
The iMessage database uses SQLite’s WAL (Write‑Ahead Logging) mode, so you will see three files:
Important behavior: when new messages arrive, chat.db-wal is updated immediately, but the main chat.db file may take several seconds or minutes to catch up (until a checkpoint is triggered).
This matters a lot for “real‑time” monitoring. If you only watch changes in chat.db, your view of new messages will lag behind.
A better approach is:
Use periodic polling instead of file‑system “change” events.
Open the database in read‑only mode and let SQLite handle the WAL file for you.
2.3 Concurrency considerations
SQLite allows concurrent readers but serializes writes. imessage-kit always opens the database in read‑only mode (readonly: true) in order to avoid disturbing Messages.app’s own operations.
3. Sending Messages: AppleScript’s Art and Limitations
3.1 Why AppleScript?
Apple does not provide an official API for iMessage. For automation, the one official option we get is AppleScript - a scripting language introduced in 1993.
A minimal example:
In practice, there are many details hiding behind this simple script.
3.2 String escaping
AppleScript is very picky about special characters. You must escape them properly.
With this function, you can safely interpolate text into AppleScript templates.
3.3 Working around sandbox constraints
Sending file attachments introduces another challenge: macOS sandboxing.
Problem: Messages.app runs in a sandbox and can only access a limited set of folders (Documents, Downloads, Pictures, etc.). If your file lives elsewhere, the send operation will fail silently or with an error.
Workaround: copy the file temporarily into ~/Pictures.
In imessage-kit, we encapsulate this pattern in a dedicated TempFileManager, which automatically scans for and cleans up files that match the imsg_temp_* pattern in ~/Pictures.
3.4 Choosing the right delay for attachments
Different file sizes need different delays to reliably upload to iCloud:
This is implemented in imessage-kit and tuned from real‑world usage.
3.5 Handling chat IDs for group messages
Sending to a group chat is more complex than sending to an individual contact, because you need to address the chat by its chatId:
How do you get chatId?
You can query it from the chat table:
About chatId formats:
Group chats use GUIDs like
chat45e2b868ce1e43da....1‑on‑1 chats may look like
iMessage;+1234567890or just+1234567890.AppleScript may expect forms such as
iMessage;+;chat45e2b868..., so you need normalization.
imessage-kit ships with built‑in normalization logic that abstracts away these differences for developers.
4. Real‑Time Monitoring: Polling and Performance
Unlike one‑off automation scripts, an iMessage agent needs to receive messages promptly and respond using an LLM or a tool‑driven policy.
Human‑computer interaction research suggests that a delay of around 500ms is already noticeable and can feel “off.” In practice, we found many pitfalls around real‑time monitoring and ended up relying on polling plus incremental queries.
4.1 Why polling instead of file‑system events?
A common first question is: why not use fs.watch or a similar mechanism to watch for new messages?
There are several reasons:
WAL mode introduces lag between the
.walfile andchat.db.File‑system events fire on many internal operations and are noisy.
Polling with timestamp‑based queries is simpler and more reliable.
A good default polling interval:
In imessage-kit, 2 seconds turned out to be a good balance between responsiveness and system overhead.
4.2 Incremental queries and de‑duplication
On each poll, we only fetch messages created after the last check:
Why have an overlap window at all?
Because clocks and commit orders are not perfect. A small overlap helps make sure messages near the boundary are not missed.
5. Cross‑Runtime Support: Bun vs Node.js
5.1 Choosing database drivers
One fun design choice in imessage-kit is native support for both Bun and Node.js:
Each runtime has its own strengths:
Bun (
bun:sqlite): built‑in, zero external dependencies, fast startup, smaller memory footprint.Node.js (
better-sqlite3): mature, battle‑tested, large ecosystem.
5.2 The zero‑dependency benefit of Bun
Using Bun lets you avoid adding an extra database dependency:
For lightweight tooling or CLIs, this can be a very nice property.
6. Real‑World Examples and Use Cases
After building imessage-kit, the Photon team immediately started dogfooding it. We even used it to take over a real iMessage account and introduce Photon to investors.
Along the way, we added more features to make it feel natural to developers: a more expressive syntax, chainable APIs, and constructs that fit agent workflows.
Here are a few concrete scenarios you can build.
6.1 An auto‑reply bot
You can wire up message content to automated responses, or integrate a full agent for intelligent handling:
6.2 Message analytics
Using the SDK also makes it easy to analyze your message history:
From here, you can layer on time‑of‑day patterns, group chat activity, attachment statistics, and more.
6.3 Webhook integrations
You can forward iMessage events into other systems such as Slack or Discord:
This is especially useful for teams that want to centralize critical alerts or customer messages.
6.4 Tracking sent messages
imessage-kit implements an OutgoingMessageManager to keep track of sent messages. Once the watcher is running, you can get the fully populated message object after sending:
How it works under the hood:
Before sending, the SDK creates a
MessagePromisewith the timestamp, content, and chatId.AppleScript executes the send.
The watcher’s polling loop detects the new message in the database.
The manager matches it by time and content and resolves the corresponding promise.
The matching logic also normalizes chat IDs (for example, between iMessage;-;recipient and recipient) so you do not have to deal with those edge cases yourself.
6.5 Automatic cleanup for temporary files
To bypass sandboxing, attachments are temporarily copied into ~/Pictures. TempFileManager is responsible for cleaning them up.
Lifecycle:
Naming convention: all temp files are prefixed with
imsg_temp_.Startup cleanup: on SDK init, any leftover temp files are removed.
Periodic cleanup: every 5 minutes, the manager scans and deletes files older than 10 minutes.
Shutdown cleanup: on SDK shutdown, all temp files are removed.
Even if the process crashes, the next run will reclaim old files.
6.6 De‑duping incoming messages
The watcher uses a Map<string, number> to track processed message IDs and avoid double‑handling:
Key points:
Use
Map(with timestamps) instead ofSetto support time‑based cleanup.Clean only when the map exceeds a threshold.
Keep about an hour of history to cap memory usage.
Combine with a 1‑second overlap window in polling to avoid edge‑case misses.
7. Pitfalls and How We Solved Them
7.1 Getting the message object right after sending
Problem: After calling send(), you cannot immediately get the full message object. The return value can be undefined.
Reason: AppleScript sends messages asynchronously, and it takes some time for the message to land in the database.
Solution:
imessage-kit’s OutgoingMessageManager uses timestamps and content to link sent messages back to database entries.
7.2 ~ in attachment paths
Attachment paths inside the database often include a leading ~:
Solution:
This ensures your script can correctly locate files on disk.
8. Performance Tuning
8.1 Query optimization
A naïve query can be very slow:
A better pattern is:
Restricting by time, ordering by recency, and limiting rows are usually enough for interactive tooling.
8.2 Controlling send concurrency
When sending many messages concurrently, a semaphore helps protect Messages.app from being overloaded:
imessage-kit includes built‑in concurrency limits (default concurrency 5) so you do not have to hand‑roll this every time.
8.3 Long‑running memory management
For long‑running watcher processes, the map of processed message IDs can grow indefinitely. imessage-kit uses an auto‑cleanup strategy:
This keeps memory bounded while still preventing duplicate handling.
9. Limits of the Current Approach
9.1 Known limitations
imessage-kit is powered by AppleScript and local database reads. As an open‑source SDK, it already covers most core operations, but because Apple has not exposed official APIs for iMessage, there are limitations that are hard to break through today, such as:
Editing messages — cannot edit messages after sending.
Message recalls — cannot recall messages within 2 minutes.
Tapbacks — can read reactions (heart, thumbs up, “ha ha”, etc.) but cannot send them.
Typing indicators — cannot send or receive typing state.
Message effects — cannot send special effects (fireworks, confetti, balloons, etc.).
Read receipts — cannot toggle or simulate read/unread status.
Stickers — cannot send iMessage stickers.
Voice messages — cannot send the waveform‑style voice messages.
On top of this, AppleScript itself has stability and concurrency limits. The whole system also depends on a user’s iCloud account and needs to run on a specific Mac, which constrains scalability.
9.2 Advanced iMessage Kit: a next‑generation foundation
After digging deeply into these limitations, we built Advanced iMessage Kit.
It uses a different system architecture to get rid of AppleScript’s constraints, unlocks nearly the full iMessage features, and runs on a dedicated infrastructure layer designed for higher concurrency and more stable service.
We have open‑sourced parts of Advanced iMessage Kit and would love to see more people bring agents into iMessage.
Conclusion
iMessage automation is a challenging but worth exploring domain. From low‑level database structure and AppleScript quirks, to macOS security and long‑running performance, every layer demands a deep understanding of how the platform works.
To make this accessible, we wrapped everything above into an open‑source, free TypeScript SDK: https://github.com/photon-hq/imessage-kit.
It turns complex iMessage operations into straightforward APIs, so you can build production‑ready workflows in minutes instead of days.
For developers who need more capabilities but do not want to maintain their own infrastructure, we also provide Advanced iMessage Kit. It unlocks more system‑level functionality while dramatically reducing deployment and configuration overhead.
We are continuing to explore what an amazing agent experiences on iMessage should feel like - from pacing and tone, to when an agent should stay quiet, to how long responses should be.
Internally, we are experimenting with even more ambitious interaction patterns. Once they are ready, we will share them with the community.
If you find any issues in https://github.com/photon-hq/imessage-kit, please feel free to open an issue or PR on GitHub. And if this project helps you, a single star is more than enough motivation for us to keep pushing this work forward.
