imsg history reads messages from a single chat in chronological order. It's the bread-and-butter command for one-shot reads — search, archive, summarize, transcribe.
#Basic read
imsg history --chat-id 42 --limit 50
imsg history --chat-id 42 --limit 50 --json | jq -s
--limit defaults to 50 and applies after filters. So --limit 20 --start ... returns up to 20 messages from inside the date window, not 20 messages globally then date-filtered.
#Date windows
imsg history --chat-id 42 \
--start 2026-05-01T00:00:00Z \
--end 2026-05-06T00:00:00Z \
--json
Both bounds accept ISO 8601 with explicit timezone. Either bound is optional:
# Everything since May 1st.
imsg history --chat-id 42 --start 2026-05-01T00:00:00Z --json
# Everything before May 6th.
imsg history --chat-id 42 --end 2026-05-06T00:00:00Z --json
#Participant filters
For group chats, narrow to messages from specific people:
imsg history --chat-id 42 --participants "+14155551212,[email protected]" --json
Match is on the message's sender (raw handle), not the resolved contact name. Pass a comma-separated list.
#Attachments
--attachments adds an attachments array to each message containing filename, UTI, MIME type, byte count, and resolved on-disk path:
imsg history --chat-id 42 --attachments --json
--convert-attachments additionally exposes model-friendly variants when ffmpeg is available — CAF audio → M4A, GIF → first-frame PNG. See Attachments.
#Recovering text from attributed bodies
Some Messages rows store rich text in a binary attributedBody column with the plain text column empty. imsg history decodes the typed-stream payload (including UTF-16LE BOM bodies) and surfaces the recovered text in the standard text field. No flag needed; this is on by default.
If a message is still empty, the source row genuinely had no text — usually a sticker, link preview, or attachment-only message.
#Reactions in history
Tapback rows (Liked "...", Loved "...", etc.) are hidden from history output by design. They'd otherwise duplicate every reacted message. To see tapbacks, use imsg watch --reactions; the live stream surfaces add and remove events with is_reaction, reaction_type, and reacted_to_guid.
#Native polls
Native Apple Messages polls are decoded when Messages stores them as the Polls extension balloon (com.apple.messages.Polls). Creation rows include poll.kind == "created" with the question and options when available. Vote update rows include poll.kind == "vote" and poll.original_guid pointing back to the poll message.
imsg history --chat-id 42 --json \
| jq -c 'select(.poll != null) | {id, guid, poll}'
Unknown or changed Polls payload variants are still emitted with poll.kind == "unknown" and raw-safe metadata. imsg does not emit the private raw payload bytes.
Native poll creation is available through the bridge:
imsg poll send --chat 'iMessage;-;+15551234567' \
--question 'Dinner?' \
--option 'Pizza' \
--option 'Sushi'
You can also use --chat-id <id> from imsg chats.
#Manual native poll test plan
- Create a native poll in Messages from an iPhone or Mac.
- Run
imsg history --chat-id <chat-id> --json | jq -c 'select(.poll != null) | {id, guid, poll}'and verify the creation row haspoll.kind == "created"with decoded question/options. - Vote on the poll from another participant/device.
- Run
imsg watch --chat-id <chat-id> --json | jq -c 'select(.poll != null)'while the vote happens, or re-run history, and verify the vote row haspoll.kind == "vote",poll.original_guidset to the original poll GUID, andpoll.vote.option_idset. - Send a poll with
imsg poll send --chat-id <id> --question "..." --option "A" --option "B"and verify it renders as a native Messages poll on iOS/macOS. - If Apple changes the private Polls payload shape, verify the row still emits
poll.kind == "unknown"with metadata and no raw payload bytes.
#Performance
JSON history batches attachment and reaction lookups in one pass per request, so large --limit values stay cheap. Reading 1000 messages with --attachments --json is bound by SQLite, not by per-row queries.
For very large reads, prefer streaming through jq rather than buffering the whole result:
imsg history --chat-id 42 --limit 5000 --json \
| jq -c 'select(.is_from_me == false)' \
> inbound.ndjson
#Message object
See JSON output for the canonical schema. Every history result has at minimum:
id, chat_id, chat_identifier, chat_guid, chat_name, participants, is_group, guid, reply_to_guid, destination_caller_id, sender, sender_name, is_from_me, text, created_at.
When --attachments is set, also: attachments[]. Native polls include poll. Reactions only appear in watch --reactions output.