parse_message silently drops image content blocks from user messages

Author: harshal-96Created Sep 19, 2026Updated Sep 19, 2026
Labelsbug

Summary

The content-block loops in parse_message (src/claude_agent_sdk/_internal/message_parser.py) have no default case, so any block type outside the handled set is dropped without a trace. The concrete victim is the image block: a user message with a pasted image parses to a UserMessage whose content is silently missing the image. There is no ImageBlock in the type model, no log line, and no way for a consumer to know the block existed.

Reproduction

python
from claude_agent_sdk._internal.message_parser import parse_message

data = {
    "type": "user",
    "message": {
        "content": [
            {"type": "text", "text": "What is in this screenshot?"},
            {"type": "image", "source": {"type": "base64", "media_type": "image/png", "data": "iVBORw0KGgo..."}},
        ]
    },
}
msg = parse_message(data)
print(len(msg.content))  # 1 on main; the image block is gone

This is the real wire shape: Claude Code emits exactly this for user messages with attached images. Verified against an actual Claude Code session transcript containing a pasted screenshot ({"type": "image", "source": {"type": "base64", "media_type": "image/png", ...}} at the top level of message.content): 2 raw blocks parse to 1 on main.

Why it is a bug rather than a modeling choice

  • Unknown message types are handled forward-compatibly with a debug log (message_parser.py, the final case _). Unknown block types get no log at all, so the two paths are inconsistent.
  • Images inside tool_result content survive, because ToolResultBlock.content passes the inner list through raw. Only top-level image blocks are lost, so a screenshot returned by a tool is visible to a consumer while a screenshot pasted by the user is not.
  • Any SDK consumer that inspects or re-processes user turns (logging, mirroring, analytics, session replay) silently loses the image content.

Expected behavior

Image blocks parse to a typed block that preserves the source payload, and still-unknown block types are skipped with a debug log like unknown message types are.

Environment

main @ 3cb0f73, Python 3.12.10, Windows. Full test suite with the fix below: 1370 passed, 14 skipped; ruff and mypy clean.

PR with the fix: #1276

Source: anthropics/claude-agent-sdk-python