Symptom
imsg send-attachment (the IMCore-bridge attachment path) registers an IMFileTransfer, dispatches a placeholder IMMessage, and reports success — but the receiver sees an empty  placeholder message with no file attached, and on the sender chat.db there's no row in attachment or message_attachment_join linking the transfer to the message. Same machine, same iMessage account, the AppleScript path works perfectly.
$ imsg send --file ~/photo.jpg --to "+15551234567" # works ✓
$ imsg send-attachment --chat 'iMessage;-;+15551234567' \
--file ~/photo.jpg # broken ✗
Messages.app UI sending also works fine — receiver gets the file. So the issue is local to our IMFileTransferCenter handling on macOS 26.4.1.
What we know
- Reproducible across small PNGs (1×1) and real-world JPEGs (178 KB) on macOS 26.4.1.
- The bridge returns
{transferGuid, messageGuid} without exception.
- chat.db has the message row (
attributedBody length ~270 B, OBJ placeholder + IM attributes), but no joined attachment row.
- Receiver sees the empty placeholder; user-attached file via Messages.app UI in the same chat is delivered correctly side-by-side.
Root-cause investigation
The relevant code is prepareOutgoingTransfer in Sources/IMsgHelper/IMsgInjected.m, modeled on BlueBubblesHelper's prepareFileTransferForAttachment:
[IMFileTransferCenter guidForNewOutgoingTransferWithLocalURL:] — returns a guid ✓
[IMFileTransferCenter transferForGUID:] — returns a non-nil IMFileTransfer ✓
[IMDPersistentAttachmentController _persistentPathForTransfer:filename:highQuality:chatGUID:storeAtExternalPath:] — returns nil on macOS 26.4.1 ✗
- (Skipped because path is nil) Copy file to persistent path
[IMFileTransferCenter retargetTransfer:toPath:] (skipped)
[IMFileTransfer setLocalURL:] (skipped)
[IMFileTransferCenter registerTransferWithDaemon:] — called with the original /tmp/... path because we never retargeted
The daemon then has a transfer guid pointing at a path Messages.app can read but apparently can't ship through to imagent properly. Receiver gets the empty placeholder.
What's been ruled out
- Path-prefix attribute issues:
__kIMFileTransferGUIDAttributeName, __kIMFilenameAttributeName, __kIMMessagePartAttributeName, __kIMBaseWritingDirectionAttributeName are all set on the OBJ placeholder, matching BlueBubblesHelper's attachmentStr exactly.
- Send flags: previously
0x5; now 0x100005 (BB-verified, the 0x100000 finalization bit). Doesn't change attachment behavior.
- Init signature: switched to BB's 12-arg
initWithSender:…:expressiveSendStyleID: (not the legacy initIMMessageWithSender:). Doesn't change attachment behavior.
- IMMessageItem-first construction: bypassed for attachments — they go through BB's IMMessage init directly. No effect on the staging gap.
- ARC zombie:
getReturnValue into __unsafe_unretained for the persistentPath was tightened to a strong reference. The path was returning nil already, before the retention change — so this wasn't the cause.
What's likely the fix
Runtime probe of IMDPersistentAttachmentController on macOS 26.4.1 reveals the modern staging selectors:
-[IMDPersistentAttachmentController saveAttachmentsForTransfer:chatGUID:storeAtExternalLocation:completion:]
-[IMDPersistentAttachmentController _saveAttachmentForTransfer:highQuality:copyWithinAttachmentStore:chatGUID:storeAtExternalPath:]
-[IMDPersistentAttachmentController _persistentPathForTransfer:filename:highQuality:chatGUID:storeAtExternalPath:]
The _persistentPath… selector is still in the class but returns nil for our IMFileTransfer. The likely modern path:
- Allocate the transfer guid (as we do today).
- Resolve
IMFileTransfer (as we do today).
- Use
saveAttachmentsForTransfer:chatGUID:storeAtExternalLocation:completion: (block-based) instead of _persistentPathForTransfer: to let the controller stage the file itself.
- Wait for the completion to fire (run-loop pump similar to
deriveThreadIdentifier).
registerTransferWithDaemon:.
Or possibly _saveAttachmentForTransfer:highQuality:copyWithinAttachmentStore:chatGUID:storeAtExternalPath: — synchronous variant. Worth class-dumping IMDPersistentAttachmentController.h from a fresh dyld_shared_cache extract on macOS 26.4.1 to get the exact signatures.
Reference
BlueBubblesHelper's prepareFileTransferForAttachment is what prepareOutgoingTransfer is modeled on. BB's most recent test target may pre-date macOS 26.
Workaround
Use the AppleScript path until the staging fix lands:
imsg send --file ~/photo.jpg --to "+15551234567"
imsg send --file does not require SIP-disabled bridge injection and is unaffected by this regression. It's the path Lobster's gateway should prefer for attachments.
Related
cc @omarshahine
🤖 Filed via Claude Code after a multi-hour debugging session
Symptom
imsg send-attachment(the IMCore-bridge attachment path) registers anIMFileTransfer, dispatches a placeholderIMMessage, and reports success — but the receiver sees an emptyplaceholder message with no file attached, and on the sender chat.db there's no row inattachmentormessage_attachment_joinlinking the transfer to the message. Same machine, same iMessage account, the AppleScript path works perfectly.Messages.appUI sending also works fine — receiver gets the file. So the issue is local to our IMFileTransferCenter handling on macOS 26.4.1.What we know
{transferGuid, messageGuid}without exception.attributedBodylength ~270 B, OBJ placeholder + IM attributes), but no joined attachment row.Root-cause investigation
The relevant code is
prepareOutgoingTransferinSources/IMsgHelper/IMsgInjected.m, modeled on BlueBubblesHelper'sprepareFileTransferForAttachment:[IMFileTransferCenter guidForNewOutgoingTransferWithLocalURL:]— returns a guid ✓[IMFileTransferCenter transferForGUID:]— returns a non-nilIMFileTransfer✓[IMDPersistentAttachmentController _persistentPathForTransfer:filename:highQuality:chatGUID:storeAtExternalPath:]— returns nil on macOS 26.4.1 ✗[IMFileTransferCenter retargetTransfer:toPath:](skipped)[IMFileTransfer setLocalURL:](skipped)[IMFileTransferCenter registerTransferWithDaemon:]— called with the original/tmp/...path because we never retargetedThe daemon then has a transfer guid pointing at a path Messages.app can read but apparently can't ship through to imagent properly. Receiver gets the empty placeholder.
What's been ruled out
__kIMFileTransferGUIDAttributeName,__kIMFilenameAttributeName,__kIMMessagePartAttributeName,__kIMBaseWritingDirectionAttributeNameare all set on the OBJ placeholder, matching BlueBubblesHelper'sattachmentStrexactly.0x5; now0x100005(BB-verified, the0x100000finalization bit). Doesn't change attachment behavior.initWithSender:…:expressiveSendStyleID:(not the legacyinitIMMessageWithSender:). Doesn't change attachment behavior.getReturnValueinto__unsafe_unretainedfor the persistentPath was tightened to a strong reference. The path was returning nil already, before the retention change — so this wasn't the cause.What's likely the fix
Runtime probe of
IMDPersistentAttachmentControlleron macOS 26.4.1 reveals the modern staging selectors:The
_persistentPath…selector is still in the class but returns nil for ourIMFileTransfer. The likely modern path:IMFileTransfer(as we do today).saveAttachmentsForTransfer:chatGUID:storeAtExternalLocation:completion:(block-based) instead of_persistentPathForTransfer:to let the controller stage the file itself.deriveThreadIdentifier).registerTransferWithDaemon:.Or possibly
_saveAttachmentForTransfer:highQuality:copyWithinAttachmentStore:chatGUID:storeAtExternalPath:— synchronous variant. Worth class-dumpingIMDPersistentAttachmentController.hfrom a fresh dyld_shared_cache extract on macOS 26.4.1 to get the exact signatures.Reference
BlueBubblesHelper's
prepareFileTransferForAttachmentis whatprepareOutgoingTransferis modeled on. BB's most recent test target may pre-date macOS 26.Workaround
Use the AppleScript path until the staging fix lands:
imsg send --filedoes not require SIP-disabled bridge injection and is unaffected by this regression. It's the path Lobster's gateway should prefer for attachments.Related
cc @omarshahine
🤖 Filed via Claude Code after a multi-hour debugging session