platform-imessage is the iMessage integration in Beeper Desktop on macOS. Designed to run with System Integrity Protection (SIP) enabled.
Built on an extended version of Platform SDK.
For local testing without the main Beeper app, use the headless Electron harness in src/SwiftServer/headless/index.ts.
yarn build:swift --debug --standalone
yarn headless:build
yarn headless:runThis starts an interactive imsg> prompt that lets you call MessagesController methods directly.
You can also run a single command without entering the REPL:
yarn headless:run "sendMessage any;-;sjobs@apple.com hello-world"
yarn headless:run "editMessage any;-;sjobs@apple.com last-message hello-world"Other REPL commands:
imsg> undoSend any;-;sjobs@apple.com B2494090-4058-4013-ACF4-6EF91E595DDD
imsg> setReaction any;-;sjobs@apple.com last-message heart true
imsg> toggleThreadRead any;-;sjobs@apple.com true
imsg> muteThread any;-;sjobs@apple.com true
imsg> notifyAnyway any;-;sjobs@apple.com
imsg> sendTypingStatus any;-;sjobs@apple.com true
imsg> watch any;-;sjobs@apple.comUse _ for arguments that should be passed through as nil/undefined. last-message resolves to the latest message ID in that thread.
The REPL currently splits command arguments on spaces, so message text containing spaces is not preserved correctly yet.
It is not possible to use local builds with Beeper Desktop. You can still interact with this project as a library, see texthsq/platform-test-lib as an example how Platform SDK can be used.
Instructions excerpt
Note
This is a snippet from our internal documentation shared as a reference. You won't be able to run this project with Beeper Desktop.
platform-imessage implements local iMessage support on macOS. This requires
various permissions that must be granted to the app. There are various pitfalls
with this:
[!IMPORTANT] When adding a local iMessage account to Beeper, you'll be prompted for several permissions. One of them is "Accessibility", which you need to grant in a System Settings window that the app opens for you. In development, grant this permission to your terminal program, text editor, or wherever you're running
yarn devfrom INSTEAD of Beeper or Electron.
[!TIP] If you're having trouble granting permissions to the app, try running:
tccutil reset All com.github.ElectronThis completely wipes away the permission state of the app with that bundle identifier in the "Privacy & Security" section of System Settings, which gives you a clean slate to work with. If that still doesn't work:
- Try passing the bundle ID of your terminal emulator, text editor, or whatever you run
yarn devin totccutilinstead ofcom.github.Electron. Example bundle identifiers:
- iTerm2:
com.googlecode.iterm2- Ghostty:
com.mitchellh.ghostty- VS Code:
com.microsoft.VSCode- Cursor:
com.todesktop.230313mzl4w4u92(yes, actually)- Try running any relevant
tccutilcommands, completely quitting and restarting all apps involved, and trying again.- Try rebooting after running the
tccutilcommand.
macOS examines the ultimately "responsible" process when deciding whether
permissions are granted or not. Because yarn dev (and therefore Electron) are
subprocesses of your terminal/text editor and the kernel is unable to know that
you ran the command yourself, the permissions must be granted there instead of
Electron itself. (This is only relevant in a development environment.)
# for debugging:
rm binaries/*/libNodeAPI.dylib # needed only when you get ENOENT
bun run build:swift --debug --watch
# for shipping to prod:
bun run build:swiftSwiftServer exposes Swift functions to JS via NAPI/node-swift and handles all invocation of native Apple methods.
-
Transparent thread merging: iMessage has separate threads for each address/sender ID (email or phone #) but transparently merges threads belonging to the same contact in the UI. Texts app shows separate threads and performs no merging. Imagine if we had two threads, stevejobs@apple.com and sjobs@apple.com. When calling a deep link to select either thread, sometimes it'll select the thread in the sidebar, other times it'd create a new compose cell. The logic is unknown.

-
MessagesController.pasteFileInBodyField: On Big Sur, usingpasteboard.setString(fileURL.relativeString, forType: .fileURL)doesn't paste the file itself but a link. Monterey has intended behavior. -
elements.selectedThreadCellis nil after pin/unpin because no cells are selected.imessage://open?message-guid=will not select the thread in sidebar (elements.selectedThreadCell == nil) if it's already open butimessage://open?address=will. -
After
elements.searchFieldwas clicked:elements.conversationsListwill be nil, selected item in sidebar will not always be reflective of the messages list, calling a deep link will not update sidebar but only the messages list,CKLastSelectedItemIdentifierwon't be updated.