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.
#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[]. Reactions only appear in watch --reactions output.