Upstream report — imessage plugin can't read HEIC/HEIF attachments
Plugin: imessage@claude-plugins-official (v0.1.0)
Platform: macOS (the plugin is already macOS-only — reads ~/Library/Messages/chat.db, sends via osascript)
Summary
When someone sends a photo over iMessage, the plugin surfaces it to the
assistant via the image_path attribute and the instruction "Read that file."
But iPhone photos default to HEIC, and Claude Code's Read tool cannot decode
HEIC/HEIF — it falls back to reading the file as text and errors. The assistant
then typically tells the sender it "can't access the attachment." In practice
this means a large fraction of real-world image attachments silently fail.
This is not a permissions issue — the file is on disk and readable. The Read
tool simply doesn't recognize .heic/.heif as an image format (it renders
PNG/JPEG/GIF/WebP).
The real fix is in the Claude Code CLI, not this plugin. The root cause is
that the CLI's Read tool can't decode HEIC/HEIF — it worked in earlier builds
and regressed around late May 2026. Restoring HEIC support in the Read tool
fixes this for every consumer (not just iMessage), and is the proper place to
fix it. The plugin-side change below is defense-in-depth: HEIC is the
default iPhone format and far too common for a messaging bridge to leave to the
Read tool's format coverage, so the plugin shouldn't hand over a path it knows
the assistant likely can't open. Ideally both ship; the CLI fix is primary.
Reproduction
- From an iPhone, send a photo (default camera format = HEIC) to a chat the
plugin watches.
- The plugin emits
image_path pointing at …/Attachments/…/IMG_xxxx.heic.
- The assistant Reads the path and gets, e.g.:
File content (1.8MB) exceeds maximum allowed size (256KB).
(or, for a small HEIC, raw binary rendered as text — i.e. not recognized as
an image at all).
Root cause
In server.ts → handleInbound (~L846–874), the attachment selection accepts
any image/* mime and passes the raw path straight through:
let imagePath: string | undefined
if (hasAttachments) {
for (const att of qAttachments.all(r.rowid)) {
if (!att.filename) continue
if (att.mime_type && !att.mime_type.startsWith('image/')) continue
imagePath = expandTilde(att.filename) // <-- could be a .heic the Read tool can't decode
break
}
}
image/heic passes the filter, so a .heic path is handed to an assistant that
can't open it.
Proposed fix (recommended): transcode HEIC/HEIF in the plugin
Convert HEIC/HEIF to JPEG with macOS's built-in sips before emitting
image_path. sips ships with macOS, so it adds no dependency to a plugin
that's already macOS-only. This is robust regardless of the consuming
assistant's instructions or the Read tool's format support.
-import { homedir } from 'os'
+import { homedir, tmpdir } from 'os'
// Claude's Read tool renders PNG/JPEG/GIF/WebP but not HEIC/HEIF — the default
// format for iPhone photos. Transcode those to JPEG via macOS `sips` so the
// assistant can actually see the image; pass other formats through unchanged.
// Returns undefined (skip the attachment) if conversion fails.
const IMG_CONVERT_DIR = join(tmpdir(), 'imessage-channel-img')
function toReadableImage(path: string): string | undefined {
if (!/\.hei[cf]$/i.test(path)) return path
try {
mkdirSync(IMG_CONVERT_DIR, { recursive: true })
const out = join(IMG_CONVERT_DIR, basename(path).replace(/\.[^.]+$/, '') + '.jpg')
const res = spawnSync('sips', ['-s', 'format', 'jpeg', '-Z', '2048', path, '--out', out], { encoding: 'utf8' })
if (res.status !== 0) {
process.stderr.write(`imessage channel: HEIC->JPEG failed for ${path}: ${res.stderr?.trim() || res.status}\n`)
return undefined
}
return out
} catch (err) {
process.stderr.write(`imessage channel: HEIC convert error: ${err}\n`)
return undefined
}
}
let imagePath: string | undefined
if (hasAttachments) {
for (const att of qAttachments.all(r.rowid)) {
if (!att.filename) continue
if (att.mime_type && !att.mime_type.startsWith('image/')) continue
- imagePath = expandTilde(att.filename)
- break
+ imagePath = toReadableImage(expandTilde(att.filename))
+ if (imagePath) break
}
}
(spawnSync, mkdirSync, join, basename are already imported in server.ts;
only tmpdir needs adding.)
Notes / tradeoffs
-Z 2048 caps the long edge at 2048px to keep the JPEG small while staying
legible; drop it if full resolution is preferred.
- Converted files accumulate under
$TMPDIR/imessage-channel-img/. Optional: a
size/age sweep, or unlink after the assistant reads it (harder to time).
- Could key off
att.mime_type (image/heic/image/heif) instead of the file
extension; extension is what the Read tool keys on, so it's the more direct
signal.
Alternative (lighter): instruction-only
If you'd rather not transcode in the polling path, update the MCP instructions
string so the assistant converts on its own when it sees a .heic/.heif path:
If the tag has an image_path attribute, it is an image the sender attached —
Read that file to view it. Exception: HEIC/HEIF (path ends .heic/.heif,
the default iPhone photo format) cannot be decoded by the Read tool; first
convert it with sips -s format jpeg "<path>" --out /tmp/imsg-attach.jpg, then
Read /tmp/imsg-attach.jpg.
This is lighter but less robust — it depends on the assistant having Bash access
and following the instruction every time. The transcode-in-plugin fix is
deterministic and assistant-agnostic.
The primary fix: restore HEIC support in the CLI Read tool
To be explicit: the real fix is CLI-side. The Read tool dropped HEIC/HEIF
support (worked in earlier builds, regressed ~late May 2026); restoring it fixes
this for every consumer of the Read tool, not just this plugin, and is the
correct root-cause fix. This issue is filed here because the plugin can ship a
robust workaround independently — but if you can route the Read-tool regression
to the CLI team, that's the fix that matters most. The plugin transcode above is
belt-and-suspenders for how common iPhone HEIC is.
Upstream report — imessage plugin can't read HEIC/HEIF attachments
Plugin:
imessage@claude-plugins-official(v0.1.0)Platform: macOS (the plugin is already macOS-only — reads
~/Library/Messages/chat.db, sends viaosascript)Summary
When someone sends a photo over iMessage, the plugin surfaces it to the
assistant via the
image_pathattribute and the instruction "Read that file."But iPhone photos default to HEIC, and Claude Code's Read tool cannot decode
HEIC/HEIF — it falls back to reading the file as text and errors. The assistant
then typically tells the sender it "can't access the attachment." In practice
this means a large fraction of real-world image attachments silently fail.
This is not a permissions issue — the file is on disk and readable. The Read
tool simply doesn't recognize
.heic/.heifas an image format (it rendersPNG/JPEG/GIF/WebP).
Reproduction
plugin watches.
image_pathpointing at…/Attachments/…/IMG_xxxx.heic.File content (1.8MB) exceeds maximum allowed size (256KB).(or, for a small HEIC, raw binary rendered as text — i.e. not recognized as
an image at all).
Root cause
In
server.ts→handleInbound(~L846–874), the attachment selection acceptsany
image/*mime and passes the raw path straight through:image/heicpasses the filter, so a.heicpath is handed to an assistant thatcan't open it.
Proposed fix (recommended): transcode HEIC/HEIF in the plugin
Convert HEIC/HEIF to JPEG with macOS's built-in
sipsbefore emittingimage_path.sipsships with macOS, so it adds no dependency to a pluginthat's already macOS-only. This is robust regardless of the consuming
assistant's instructions or the Read tool's format support.
let imagePath: string | undefined if (hasAttachments) { for (const att of qAttachments.all(r.rowid)) { if (!att.filename) continue if (att.mime_type && !att.mime_type.startsWith('image/')) continue - imagePath = expandTilde(att.filename) - break + imagePath = toReadableImage(expandTilde(att.filename)) + if (imagePath) break } }(
spawnSync,mkdirSync,join,basenameare already imported inserver.ts;only
tmpdirneeds adding.)Notes / tradeoffs
-Z 2048caps the long edge at 2048px to keep the JPEG small while stayinglegible; drop it if full resolution is preferred.
$TMPDIR/imessage-channel-img/. Optional: asize/age sweep, or
unlinkafter the assistant reads it (harder to time).att.mime_type(image/heic/image/heif) instead of the fileextension; extension is what the Read tool keys on, so it's the more direct
signal.
Alternative (lighter): instruction-only
If you'd rather not transcode in the polling path, update the MCP
instructionsstring so the assistant converts on its own when it sees a
.heic/.heifpath:This is lighter but less robust — it depends on the assistant having Bash access
and following the instruction every time. The transcode-in-plugin fix is
deterministic and assistant-agnostic.
The primary fix: restore HEIC support in the CLI Read tool
To be explicit: the real fix is CLI-side. The Read tool dropped HEIC/HEIF
support (worked in earlier builds, regressed ~late May 2026); restoring it fixes
this for every consumer of the Read tool, not just this plugin, and is the
correct root-cause fix. This issue is filed here because the plugin can ship a
robust workaround independently — but if you can route the Read-tool regression
to the CLI team, that's the fix that matters most. The plugin transcode above is
belt-and-suspenders for how common iPhone HEIC is.