关于 imessage-query
面向 Claude Code 用户的 macOS iMessage 查询技能,可检索聊天记录、解码二进制消息并整理带时间戳的来源时间线。它提供只读查询流程和多层解码脚本,适用于分析隐藏在 attributedBody 中的文本。
主要功能
- 01查询联系人聊天记录
- 02解码 attributedBody 文本
- 03按关键词搜索并查看上下文
- 04导出 NDJSON 消息
- 05识别编辑、附件和音频消息
适用场景
- 01整理带时间戳的对话时间线
- 02搜索跨对话的关键词
- 03恢复 text 字段为空的消息
- 04将聊天记录导出后离线分析
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ terrylica/ cc-skills --skill imessage-query安装后在 Claude Code 中输入/imessage-query确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
references/cross-repo-analysis.mdevolution-log.mdknown-pitfalls.mdquery-patterns.mdschema-reference.mdscripts/decode_attributed_body.py详细介绍
iMessage 数据库查询
查询 macOS iMessage SQLite 数据库(~/Library/Messages/chat.db),获取对话历史、解码以二进制格式存储的消息,并构建带有精确时间戳且注明来源的时间线。
自我演进的技能:此技能会在使用中不断改进。如果说明有误、参数已变更,或需要采用变通方法,请立即修复此文件,不要拖延。仅针对真实且可复现的问题进行更新。
适用场景
- 获取特定联系人的 iMessage 对话历史
- 根据短信中的时间戳构建注明来源的时间线
- 搜索所有对话中的关键词
- 排查显示为空但实际包含可恢复文本的消息
- 提取 iOS 以二进制
attributedBody格式存储的消息内容
前提条件
- 仅限 macOS —
chat.db是 macOS 专用数据库 - 完全磁盘访问权限 — 运行 Claude Code 的终端必须在“系统设置 > 隐私与安全性 > 完全磁盘访问权限”中获授 FDA
- 只读 — 切勿写入
chat.db。始终使用只读 SQLite 访问方式。 - 可选:
pip install pytypedstream— 启用第 1 层解码器(正确反序列化 typedstream)。不安装也能运行脚本(会继续使用纯二进制的第 2/3 层)。
关键知识——text 与 attributedBody 的问题
重要:许多 iMessage 消息的 text 列为 NULL 或为空,但 attributedBody 列中包含有效且可恢复的文本。这并非因为这些消息是语音消息——iOS 会将听写消息、带富文本格式的消息以及部分普通消息以 NSAttributedString 二进制数据块的形式存储在 attributedBody 中。
如何检测
-- Messages with attributedBody but no text (these are NOT necessarily voice messages)
SELECT COUNT(*) as hidden_messages
FROM message m
JOIN chat_message_join cmj ON m.ROWID = cmj.message_id
JOIN chat c ON cmj.chat_id = c.ROWID
WHERE c.chat_identifier = '<CHAT_IDENTIFIER>'
AND (m.text IS NULL OR length(m.text) = 0)
AND m.attributedBody IS NOT NULL
AND length(m.attributedBody) > 100
AND m.associated_message_type = 0
AND m.cache_has_attachments = 0;
text 为 NULL 时如何区分消息类型
cache_has_attachments | attributedBody 长度 | 可能的类型 |
|---|---|---|
| 0 | > 100 字节 | 听写/富文本 — 可通过解码脚本恢复 |
| 1 | 任意 | 附件(图像、文件、语音备忘录)——attributedBody 中也可能包含文本 |
| 0 | < 50 字节 | Tapback 表情回应或系统消息——通常是无关内容 |
如何解码
使用随附的解码脚本可靠地提取内容(v4 — 3 层解码器 + 原生格式陷阱防护):
python3 <skill-path>/scripts/decode_attributed_body.py --chat "<CHAT_IDENTIFIER>" --limit 50
解码器采用 3 层策略:
- 第 1 层:
pytypedstreamUnarchiver — 正确反序列化 Apple typedstream(需要pip install pytypedstream) - 第 2 层:多格式二进制 — 解析 0x2B/0x4F/0x49 长度前缀(零依赖,移植自 macos-messages)
- 第 3 层:NSString 标记 + 长度前缀 — v2 旧版方法(零依赖,最后手段)
某一层失败时会继续尝试下一层。未安装 pytypedstream 也能运行(会跳过第 1 层)。解码器对比请参阅跨仓库分析。
日期公式
iMessage 将日期存储为自 Apple 纪元(2001-01-01 00:00:00 UTC)起的纳秒数。
datetime(m.date/1000000000 + 978307200, 'unixepoch', 'localtime') as timestamp
m.date / 1000000000— 将纳秒转换为秒+ 978307200— 加上 Unix 纪元(1970)到 Apple 纪元(2001)的偏移量'unixepoch'— 告知 SQLite 这是 Unix 时间戳'localtime'— 转换为本地时区(至关重要——省略后会得到 UTC)
快速入门查询
1. 列出所有对话
sqlite3 ~/Library/Messages/chat.db \
"SELECT c.chat_identifier, c.display_name, COUNT(cmj.message_id) as msg_count
FROM chat c
JOIN chat_message_join cmj ON c.ROWID = cmj.chat_id
GROUP BY c.ROWID
ORDER BY msg_count DESC
LIMIT 20"
2. 获取对话线程(仅 text 列)
sqlite3 ~/Library/Messages/chat.db \
"SELECT datetime(m.date/1000000000 + 978307200, 'unixepoch', 'localtime') as ts,
CASE WHEN m.is_from_me = 1 THEN 'Me' ELSE 'Them' END as sender,
m.text
FROM message m
JOIN chat_message_join cmj ON m.ROWID = cmj.message_id
JOIN chat c ON cmj.chat_id = c.ROWID
WHERE c.chat_identifier = '<CHAT_IDENTIFIER>'
AND length(m.text) > 0
AND m.associated_message_type = 0
ORDER BY m.date DESC
LIMIT 50"
3. 获取所有消息,包括 attributedBody(使用解码脚本)
python3 <skill-path>/scripts/decode_attributed_body.py \
--chat "<CHAT_IDENTIFIER>" \
--after "2026-01-01" \
--limit 100
过滤无关内容
Tapback 表情回应
Tapback 表情回应(赞、爱心、强调等)会存储为单独的消息行,且 associated_message_type != 0。请务必过滤:
AND m.associated_message_type = 0
zsh 中的 Shell 转义
!= 运算符在 zsh 中可能导致问题。请改用肯定断言:
-- BAD (breaks in zsh)
AND m.text != ''
-- GOOD (works everywhere)
AND length(m.text) > 0
使用解码脚本
随附的 decode_attributed_body.py 可处理所有边缘情况:
# Basic usage - get last 50 messages from a contact
python3 <skill-path>/scripts/decode_attributed_body.py --chat "+1234567890" --limit 50
# Search for keyword
python3 <skill-path>/scripts/decode_attributed_body.py --chat "+1234567890" --search "meeting"
# Search with surrounding context (3 messages before and after each match)
python3 <skill-path>/scripts/decode_attributed_body.py --chat "+1234567890" --search "meeting" --context 3
# Date range
python3 <skill-path>/scripts/decode_attributed_body.py --chat "+1234567890" --after "2026-01-01" --before "2026-02-01"
# Only messages from the other party
python3 <skill-path>/scripts/decode_attributed_body.py --chat "+1234567890" --sender them
# Only messages from me
python3 <skill-path>/scripts/decode_attributed_body.py --chat "+1234567890" --sender me
# Export conversation to NDJSON for offline analysis
python3 <skill-path>/scripts/decode_attributed_body.py --chat "+1234567890" --after "2026-02-01" --export thread.jsonl
输出格式:timestamp|sender|text(以竖线分隔,每行一条消息)
上下文搜索(--context N)
将 --search 与 --context N 结合使用时,脚本会显示每个匹配项前后各 N 条消息:
- 匹配项前缀为
[match] - 不连续的上下文组以
--- context ---分隔 - 重叠的上下文窗口会去重
NDJSON 导出(--export)
将消息导出到 NDJSON(.jsonl)文件,以供离线分析:
{
"ts": "2026-02-13 18:30:17",
"sender": "them",
"is_from_me": false,
"text": "Message text here",
"decoded": true,
"type": "text",
"edited": true,
"service": "SMS",
"effect": "slam",
"reply_to": {
"ts": "2026-02-13 18:00:00",
"sender": "me",
"text": "Original message..."
}
}
字段 edited、service、effect、reply_to 为可选字段——仅在适用时出现。type 字段始终存在("text"、"audio" 或 "attachment")。
撤回的消息绝不会被导出——它们会被确定性地排除(见下文的原生保护措施)。
先导出再分析的工作流(推荐用于多次查询分析):
# Step 1: Export once
python3 <skill-path>/scripts/decode_attributed_body.py --chat "+1234567890" \
--after "2026-02-01" --export thread.jsonl
# Step 2: Analyze many times without re-querying SQLite
grep -i "keyword" thread.jsonl
jq 'select(.text | test("reference"; "i"))' thread.jsonl
jq 'select(.sender == "them")' thread.jsonl
原生保护措施(v4)
解码脚本会原生处理以下陷阱——无需手动编写 SQL 变通方案:
| 保护措施 | 使用的列 | 行为 |
|---|---|---|
| 撤回的消息(撤回发送) | date_retracted、date_edited | 从输出中排除——内容已被 iOS 清除,不可采信 |
| 已编辑的消息 | date_edited | 标记为 [edited] / "edited": true |
| 音频/语音消息 | is_audio_message | 标识为 [audio message]——不会被误分类为空消息 |
| 内嵌引用(滑动回复) | thread_originator_guid | 通过 GUID 索引解析为被引用的消息文本 |
| 没有文本的附件 | cache_has_attachments、附件表 | 显示为 [attachment: filename],而不是被静默丢弃 |
| 消息效果 | expressive_send_style_id | 解码为易读名称(slam、loud、gentle、invisible_ink) |
| 服务类型 | service | 当使用 SMS 而非 iMessage 时进行标记 |
| Tapback 表情回应 | associated_message_type | 进行过滤(仅包含 = 0 的消息) |
应避免的反模式
- 盲目搜索多个聊天标识符——始终先运行
--stats,确认正确的聊天标识符在预期日期范围内包含消息 - 不带上下文的关键词搜索——使用
--search时,始终搭配--context 5(或更大的值),以理解匹配项周围的对话含义 - 重复执行狭窄时间窗口的 SQLite 查询——先将完整日期范围导出为 NDJSON,再使用 grep/jq 对该文件进行后续所有分析
注意:将 <skill-path> 替换为实际安装的技能路径。查找方法如下:
find ~/.claude -path "*/imessage-query/scripts/decode_attributed_body.py" 2>/dev/null
参考文档
TodoWrite 任务模板
模板 A - 检索对话线程
1. Identify chat_identifier for the contact (phone number or email)
2. Run decode script with --chat and appropriate date range
3. Review output for attributedBody-decoded messages (marked with [decoded])
4. If searching for specific topic, add --search flag
5. Format results as needed for the task
模板 B - 调试空消息
1. Query messages where text IS NULL but attributedBody IS NOT NULL
2. Check cache_has_attachments to distinguish voice/file from dictated text
3. Run decode script to extract hidden text content
4. Verify decoded content makes sense in conversation context
5. Document any new decode patterns in known-pitfalls.md
模板 C - 构建带来源的时间线
1. Identify all relevant chat_identifiers
2. Run decode script for each contact with date range
3. Merge and sort by timestamp
4. Format as sourced quotes with timestamps for documentation
5. Verify no messages were missed (compare total count vs decoded count)
模板 D - 先导出再进行深度分析
1. Run --stats to confirm chat_identifier and date range
2. Export full date range to NDJSON: --export thread.jsonl
3. Use grep/jq on the NDJSON file for all keyword searches
4. Use --search with --context 5 for contextual understanding of specific matches
5. All subsequent analysis reads from the NDJSON file (no more SQLite queries)
变更后检查清单
修改此技能后:
- YAML frontmatter 有效(包含 name 和带触发条件的 description)
- 所有文件中均无私人数据(电话号码、姓名、电子邮件)
- 所有 SQL 均使用参数化占位符
- 解码脚本可使用
python3运行(pytypedstream 为可选项,第 2/3 层仅使用标准库) - 所有参考链接均为相对路径
- 将变更追加到 evolution-log.md
执行后复盘
此技能运行完成后,在结束前检查:
- 命令是否成功? — 如果没有,请修正导致失败的指令或错误表。
- 参数或输出是否发生变化? — 如果底层工具的接口发生变化,请更新用法示例和参数表,使其保持一致。
- 是否需要变通方案? — 如果你不得不临时变通(更换标志、增加步骤),请更新此 SKILL.md,确保下次调用时不必再采用相同的变通方案。
仅当问题真实存在且可复现时才更新,不要基于猜测进行更新。