claude/skills/imsg/SKILL.md
iMessage/SMS CLI for listing chats, history, and sending messages via Messages.app.
npx skillsauth add kendreaditya/.config imsgInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Use imsg to read and send iMessage/SMS via macOS Messages.app.
✅ USE this skill when:
❌ DON'T use this skill when:
message tool with channel:telegrammessage tool with channel:discordslack skillimsg chats --limit 10 --json
# By chat ID
imsg history --chat-id 1 --limit 20 --json
# With attachments info
imsg history --chat-id 1 --limit 20 --attachments --json
imsg watch --chat-id 1 --attachments
# Text only
imsg send --to "+14155551212" --text "Hello!"
# With attachment
imsg send --to "+14155551212" --text "Check this out" --file /path/to/image.jpg
# Specify service
imsg send --to "+14155551212" --text "Hi" --service imessage
imsg send --to "+14155551212" --text "Hi" --service sms
--service imessage — Force iMessage (requires recipient has iMessage)--service sms — Force SMS (green bubble)--service auto — Let Messages.app decide (default)User: "Text mom that I'll be late"
# 1. Find mom's chat
imsg chats --limit 20 --json | jq '.[] | select(.displayName | contains("Mom"))'
# 2. Confirm with user
# "Found Mom at +1555123456. Send 'I'll be late' via iMessage?"
# 3. Send after confirmation
imsg send --to "+1555123456" --text "I'll be late"
imsg reads from ~/Library/Messages/chat.db (same source), but requires a numeric --chat-id from imsg chats. It cannot:
chat_message_join (some synced messages are handle-only)Note: imsg chats output is newline-delimited JSON (NDJSON), not a JSON array — parse line by line.
Decision guide:
| Task | Tool |
|---|---|
| Read recent messages, watch live | imsg history / watch |
| Send a message | imsg send |
| Find which group chats a contact is in | SQL (handle + chat_handle_join) |
| Bulk export all messages to file | Manual SQL + Python |
| Merge phone + email handles for one contact | Manual SQL |
| Get reactions as structured data | imsg (reactions field in JSON output) |
Database: ~/Library/Messages/chat.db
Key tables: handle (contact identifiers), message (texts + blobs), chat_handle_join, chat_message_join, attachment
# Find all handles (phone/email) for a contact
sqlite3 ~/Library/Messages/chat.db \
"SELECT ROWID, id, service FROM handle WHERE id LIKE '%search_term%';"
# Find which chats a contact participates in
sqlite3 ~/Library/Messages/chat.db "
SELECT c.ROWID, c.chat_identifier, c.display_name, COUNT(cmj.message_id) as msgs
FROM chat c
JOIN chat_handle_join chj ON c.ROWID = chj.chat_id
JOIN handle h ON chj.handle_id = h.ROWID
LEFT JOIN chat_message_join cmj ON c.ROWID = cmj.chat_id
WHERE h.id IN ('+1XXXXXXXXXX', '[email protected]')
GROUP BY c.ROWID ORDER BY msgs DESC;"
Decoding NSAttributedString blobs — messages where text is NULL store content as binary blobs:
import sqlite3, os, json
from datetime import datetime, timezone, timedelta
APPLE_EPOCH = datetime(2001, 1, 1, tzinfo=timezone.utc)
def decode_blob(blob):
"""Extract plain text from NSAttributedString streamtyped blob."""
if not blob: return None
raw = bytes(blob)
marker = b'\x01\x94\x84\x01\x2b'
idx = raw.find(marker)
if idx == -1: return None
pos = idx + len(marker)
lb = raw[pos]; pos += 1
if lb == 0x81: length = (raw[pos] << 8) | raw[pos+1]; pos += 2
elif lb == 0x82: length = int.from_bytes(raw[pos:pos+4], 'big'); pos += 4
else: length = lb
return raw[pos:pos+length].decode('utf-8', errors='replace')
def apple_ts(ts):
if not ts: return ""
return (APPLE_EPOCH + timedelta(seconds=ts/1_000_000_000)).astimezone().strftime("%Y-%m-%d %H:%M:%S %Z")
# Export all messages from multiple handles (phone + email) merged and sorted
DB = os.path.expanduser("~/Library/Messages/chat.db")
conn = sqlite3.connect(DB)
conn.row_factory = sqlite3.Row
cur = conn.cursor()
HANDLES = ["+1XXXXXXXXXX", "[email protected]"]
placeholders = ",".join("?" * len(HANDLES))
cur.execute(f"""
SELECT m.date, m.is_from_me, m.text, m.attributedBody, h.id, m.service
FROM message m JOIN handle h ON m.handle_id = h.ROWID
WHERE h.id IN ({placeholders})
ORDER BY m.date ASC
""", HANDLES)
messages = []
for row in cur.fetchall():
text = row["text"] or decode_blob(row["attributedBody"]) or ""
messages.append({
"date": apple_ts(row["date"]),
"from": "Me" if row["is_from_me"] else row["id"],
"text": text,
"service": row["service"]
})
with open(os.path.expanduser("~/Desktop/imessage_export.json"), "w") as f:
json.dump(messages, f, ensure_ascii=False, indent=2)
development
Search and read content from leetcode.com — problem catalog, daily challenge, full problem statements with hints and starter code, the Discuss forum (interview experiences, comp posts, layoff threads), and company question-list metadata. Read-only, no auth, no API key. Use when the user wants to look up a LeetCode problem by name/number/slug, see today's daily challenge, search Discuss for interview write-ups at a specific company (Google, Waymo, Meta, Amazon, etc.), browse a tag-filtered discuss feed, read a Discuss post + comments, or check what a LeetCode company list covers. Triggers — "lcsearch", "leetcode search", "search leetcode", "leetcode discuss", "leetcode problem", "daily leetcode", "interview discuss", "what's the leetcode for X", URLs containing leetcode.com/problems/, leetcode.com/discuss/, or leetcode.com/company/. Pair with the `interviewcoder` skill (structured leetcode-style writeups from 1point3acres) and `blind` (anonymous workplace chatter) for the same companies.
development
Terminal Spotify playback/search via spogo (preferred) or spotify_player.
development
Search and read posts from interviewcoder.co — a Next.js-fronted aggregator of technical-interview writeups (largely sourced from 1point3acres) tagged by company, position, stage (Phone Screen / OA / Onsite / etc.), period, job type, and structured leetcode-style questions. Use when the user wants real interview questions for a specific company, recent writeups from a hiring loop, leetcode-style problems with tags and difficulty, or to look up a specific interviewcoder.co URL. Read-only, no auth, no API key. Triggers — "interviewcoder", "interviewcoder.co", "interview questions at [company]", "what's been asked at [company] recently", "interview writeup", and URLs containing interviewcoder.co.
tools
Small Yahoo Finance CLI for ticker info + N-year stock returns. Use when the user asks about: stock price, market cap, sector/industry classification, dividend yield, P/E ratio, beta, 52-week range, N-year stock return, company description for a public company. Triggers: 'yfinance', 'yfin', 'stock price', 'market cap of', 'how much has X stock returned', 'sector for ticker', 'industry classification'. Pairs with the levels-fyi skill for cross-checking public/private status (levels gives ticker, yfin returns live data).