Skip to content

imessage: HEIC/HEIF photo attachments can't be read (iPhone's default format) #2286

Description

@hinderberg

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

  1. From an iPhone, send a photo (default camera format = HEIC) to a chat the
    plugin watches.
  2. The plugin emits image_path pointing at …/Attachments/…/IMG_xxxx.heic.
  3. 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.tshandleInbound (~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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions