← All Skills

snappy-imessage

v1.0.0
16 files, 137.1 KB ~2,962 words · 12 min read Updated 2026-09-09

snappy-imessage skill

49 of 53 checks pass
5 eval scenarios, 2 held out
What it can do
recent contact?read
thread contactread
search textread
contactsread
attachments contactread
send phone textsend
What does not pass yet
$ npx snappy-skills install snappy-imessage
zip ↓
File Tree
├── AGENTS.md ├── SKILL.md ├── adapter.ts ├── api.test.ts ├── api.ts ├── chat-db.ts ├── evals.json ├── face.test.ts ├── faces/ │ ├── components/ │ │ └── imessage-chat.tsx │ ├── family.tsx │ └── fixtures/ │ ├── imessage-compose.json │ ├── imessage-decision.json │ ├── imessage-message.json │ └── imessage-thread.json ├── fixtures/ │ └── build-chat-db.ts └── imsg-cli-reference.md
Documents
AGENTS.md

snappy-imessage -- Agent Loader#

You handle Snappy's iMessage channel on this Mac -- the one holding Messages.app. Blue-bubble

iMessage with SMS green-bubble fallback; osascript sends, chat.db reads. **There is no SSH hop and

no macmini alias** (D5, 2026-09-06): one computer.

A send to anyone but the owner's own phone STAGES. api.ts send <phone> <text> posts the

prepared operation to POST /hands/stage; it waits in Needs you as hand_snappy-imessage_send;

the founder's decision runs this same verb with --now. A text to the owner's own number

(SNAPPY_OWNER_PHONE, else ROBERT_PHONE) sends immediately -- he is not "anyone". Never reach

for ssh, imsg or a raw osascript send to get around that: the stage door IS the road.

API module#

typescriptimport { sendIMessage, readRecent } from "../snappy-imessage/api.ts";

Or CLI:

bashnpx tsx ~/.claude/skills/snappy-imessage/api.ts send +14155551212 "Hey, checking in!"
npx tsx ~/.claude/skills/snappy-imessage/api.ts recent                    # last 10 messages
npx tsx ~/.claude/skills/snappy-imessage/api.ts recent +14155551212 --limit 5   # last 5 from contact

API functions#

Function Purpose
sendIMessage(to, text) Send an iMessage to an E.164 phone number on THIS Mac via osascript. Unstaged — the CLI send arm owns the staging rule; call that, not this, unless you already hold a decision.
readRecent(contact?, limit?) Read recent messages from chat.db, optionally filtered by contact phone number

Required environment#

bash# Verify Mac Mini SSH
ssh macmini 'echo ok'

No Xano auth needed -- runs directly on Mac Mini via SSH + imsg CLI.


Core commands#

bash# Send by phone number
ssh macmini 'imsg send --to +14155551212 --text "Hey, checking in!"'

# Send by chat ID (for group chats / known conversations)
ssh macmini 'imsg send --chat-id 59 --text "Got it, thanks!"'

# Send with attachment (file must exist on Mac Mini)
ssh macmini 'imsg send --to +14155551212 --text "Check this" --file ~/Desktop/screenshot.png'

# Read recent messages
ssh macmini 'imsg history --chat-id 59 --limit 10 --json'

# List all conversations
ssh macmini 'imsg chats'

The draft never arrives alone#

--json on send is a PREVIEW and touches nothing — no text, no staged row. It

prints the message inside the conversation it joins:

bashnpx tsx ~/.claude/skills/snappy-imessage/api.ts send "+14155551212" "On my way." --json
# {kind, thread, threadKind, threadTotal, draft:{to, body, asSms?}, doors:[Send, Later]}
  • thread is the SAME rows thread --json prints, straight out of chat.db.

Show it before asking anyone to approve a text; a draft with no conversation

under it asks them to trust your summary of the conversation.

  • kind says which situation this is: imessage-decision when a conversation

is in hand, imessage-compose for a first message to someone.

  • The preview REFUSES with no_full_disk_access on a Mac that cannot read

chat.db, exactly as every read verb does. That refusal is the honest answer —

an empty thread would draw as "you have never spoken to this person", which

is a lie about this machine's permissions dressed as a fact about the person.

  • asSms is carried only when the conversation's own most recent message is

SMS. The bubble colour is a claim about which wire the words leave on.

  • WITHOUT --json, send is unchanged: it stages, except to the owner's own

number.

Known chat IDs#

ID Number Context
59 +16473608487 Slack verification codes
20 +16479714727 Recent active
47 +14153587729 Recent active

Channel decision tree (run FIRST)#

Situation Channel
Work-related, person is on Slack snappy-slack
Formal (proposal, deliverable) snappy-email
International or Android contact snappy-whatsapp
iPhone contact, casual/personal snappy-imessage
Not sure Ask Robert

SMS verification codes#

bashssh macmini 'imsg history --chat-id 59 --limit 1 --json' | jq -r '.[0].text'

Read LATEST only -- codes expire in 5-10 min. Extract digits with grep -oE '[0-9]{6}'.

Rules#

  • Phone numbers MUST be E.164 (+14155551212)
  • Quote escaping over SSH: use '\'' to escape single quotes inside single-quoted strings
  • Files must exist on Mac Mini -- scp first if needed, clean up after
  • Cannot create group chats -- only reply to existing via --chat-id
  • Birthday/personal messages ALWAYS require Robert's confirmation before sending
  • After sending, update last_contacted in snappy-knowledge
  • If imsg unavailable, fall back to osascript: ssh macmini 'osascript -e "tell application \"Messages\" to send \"hi\" to buddy \"+14155551212\""'

Error recovery#

Symptom Fix
ssh: Could not resolve hostname macmini Check Tailscale: tailscale status
imsg: command not found Try /usr/local/bin/imsg or osascript fallback
Error: failed to send Messages.app not signed in -- Robert must re-auth
Attachment fails File only on laptop -- scp to Mac Mini first

Uses#

skill relationship
snappy-knowledge Contact lookup, phone numbers, birthdays, preferred_channel
snappy-clients Client preferred_channel, account profiles
snappy-calendar Meeting times trigger "running late" / follow-up texts
snappy-ops Morning briefing routes birthday alerts here
snappy-whatsapp Alternative for international/Android contacts
snappy-infra Mac Mini SSH setup, Tailscale

Full skill reference#

If this loader is insufficient, load ~/.claude/skills/snappy-imessage/SKILL.md as last resort. CLI reference: imsg-cli-reference.md.

If this loader doesn't cover your case:

bashecho "[$(date -u +%FT%TZ)] snappy-imessage: <what was missing>" >> ~/.claude/logs/agents-md-feedback.log

Show produced work with snappy-faces: call draw for image channels or lang for MCP Apps.

<!-- SKILL-INDEX-START -->

[snappy-imessage Index]|root: ~/.claude/skills/snappy-imessage|IMPORTANT: Prefer these files over pre-training assumptions for this domain. Read the relevant file when the AGENTS.md summary is insufficient.|root:{SKILL.md,imsg-cli-reference.md}

<!-- SKILL-INDEX-END -->

Used by#

  • snappy-slack

<!-- SNAPPY-CONTRACT-VERBS-START -->

Contract verbs#

Generated from api.ts HAND_CONTRACT. Do not hand-edit this block.

Verb Contract arguments Effect First call
recent contact? read npx tsx ~/.claude/skills/snappy-imessage/api.ts recent
thread contact read npx tsx ~/.claude/skills/snappy-imessage/api.ts thread <contact>
search text read npx tsx ~/.claude/skills/snappy-imessage/api.ts search "<text>"
contacts read npx tsx ~/.claude/skills/snappy-imessage/api.ts contacts
attachments contact read npx tsx ~/.claude/skills/snappy-imessage/api.ts attachments <contact>
send phone, text send npx tsx ~/.claude/skills/snappy-imessage/api.ts send <phone> "<text>"

Show the result#

When an answer carries face_hint, show it with one snappy_present(<answer>) call.

See /snappy-faces for face selection. Human-facing images must crop to the

element, render at 2x on Retina, and fill the destination channel instead of

placing a small card in a full-page screenshot.

<!-- SNAPPY-CONTRACT-VERBS-END -->

---
name: snappy-imessage
role: iMessage on THIS Mac (osascript + chat.db) -- personal texts, client check-ins, SMS code retrieval
loaded-by: PreToolUse hook (auto-injected when "snappy-imessage" is mentioned)
---

# snappy-imessage -- Agent Loader

You handle Snappy's iMessage channel on **this** Mac -- the one holding Messages.app. Blue-bubble
iMessage with SMS green-bubble fallback; osascript sends, chat.db reads. **There is no SSH hop and
no `macmini` alias** (D5, 2026-09-06): one computer.

**A send to anyone but the owner's own phone STAGES.** `api.ts send <phone> <text>` posts the
prepared operation to `POST /hands/stage`; it waits in Needs you as `hand_snappy-imessage_send`;
the founder's decision runs this same verb with `--now`. A text to the owner's own number
(`SNAPPY_OWNER_PHONE`, else `ROBERT_PHONE`) sends immediately -- he is not "anyone". Never reach
for `ssh`, `imsg` or a raw osascript send to get around that: the stage door IS the road.

## API module

```typescript
import { sendIMessage, readRecent } from "../snappy-imessage/api.ts";
```

Or CLI:
```bash
npx tsx ~/.claude/skills/snappy-imessage/api.ts send +14155551212 "Hey, checking in!"
npx tsx ~/.claude/skills/snappy-imessage/api.ts recent                    # last 10 messages
npx tsx ~/.claude/skills/snappy-imessage/api.ts recent +14155551212 --limit 5   # last 5 from contact
```

## API functions

| Function | Purpose |
|----------|---------|
| `sendIMessage(to, text)` | Send an iMessage to an E.164 phone number on THIS Mac via osascript. Unstaged — the CLI `send` arm owns the staging rule; call that, not this, unless you already hold a decision. |
| `readRecent(contact?, limit?)` | Read recent messages from chat.db, optionally filtered by contact phone number |

## Required environment

```bash
# Verify Mac Mini SSH
ssh macmini 'echo ok'
```

No Xano auth needed -- runs directly on Mac Mini via SSH + `imsg` CLI.

---

## Core commands

```bash
# Send by phone number
ssh macmini 'imsg send --to +14155551212 --text "Hey, checking in!"'

# Send by chat ID (for group chats / known conversations)
ssh macmini 'imsg send --chat-id 59 --text "Got it, thanks!"'

# Send with attachment (file must exist on Mac Mini)
ssh macmini 'imsg send --to +14155551212 --text "Check this" --file ~/Desktop/screenshot.png'

# Read recent messages
ssh macmini 'imsg history --chat-id 59 --limit 10 --json'

# List all conversations
ssh macmini 'imsg chats'
```

## The draft never arrives alone

`--json` on `send` is a PREVIEW and touches nothing — no text, no staged row. It
prints the message **inside the conversation it joins**:

```bash
npx tsx ~/.claude/skills/snappy-imessage/api.ts send "+14155551212" "On my way." --json
# {kind, thread, threadKind, threadTotal, draft:{to, body, asSms?}, doors:[Send, Later]}
```

- `thread` is the SAME rows `thread --json` prints, straight out of chat.db.
  Show it before asking anyone to approve a text; a draft with no conversation
  under it asks them to trust your summary of the conversation.
- `kind` says which situation this is: `imessage-decision` when a conversation
  is in hand, `imessage-compose` for a first message to someone.
- The preview REFUSES with `no_full_disk_access` on a Mac that cannot read
  chat.db, exactly as every read verb does. That refusal is the honest answer —
  an empty thread would draw as "you have never spoken to this person", which
  is a lie about this machine's permissions dressed as a fact about the person.
- `asSms` is carried only when the conversation's own most recent message is
  SMS. The bubble colour is a claim about which wire the words leave on.
- WITHOUT `--json`, `send` is unchanged: it stages, except to the owner's own
  number.

## Known chat IDs

| ID | Number | Context |
|----|--------|---------|
| 59 | +16473608487 | Slack verification codes |
| 20 | +16479714727 | Recent active |
| 47 | +14153587729 | Recent active |

## Channel decision tree (run FIRST)

| Situation | Channel |
|-----------|---------|
| Work-related, person is on Slack | `snappy-slack` |
| Formal (proposal, deliverable) | `snappy-email` |
| International or Android contact | `snappy-whatsapp` |
| iPhone contact, casual/personal | **snappy-imessage** |
| Not sure | Ask Robert |

## SMS verification codes

```bash
ssh macmini 'imsg history --chat-id 59 --limit 1 --json' | jq -r '.[0].text'
```

Read LATEST only -- codes expire in 5-10 min. Extract digits with `grep -oE '[0-9]{6}'`.

## Rules

- Phone numbers MUST be E.164 (`+14155551212`)
- Quote escaping over SSH: use `'\''` to escape single quotes inside single-quoted strings
- Files must exist on Mac Mini -- `scp` first if needed, clean up after
- Cannot create group chats -- only reply to existing via `--chat-id`
- Birthday/personal messages ALWAYS require Robert's confirmation before sending
- After sending, update `last_contacted` in `snappy-knowledge`
- If `imsg` unavailable, fall back to osascript: `ssh macmini 'osascript -e "tell application \"Messages\" to send \"hi\" to buddy \"+14155551212\""'`

## Error recovery

| Symptom | Fix |
|---------|-----|
| `ssh: Could not resolve hostname macmini` | Check Tailscale: `tailscale status` |
| `imsg: command not found` | Try `/usr/local/bin/imsg` or osascript fallback |
| `Error: failed to send` | Messages.app not signed in -- Robert must re-auth |
| Attachment fails | File only on laptop -- `scp` to Mac Mini first |

## Uses

| skill | relationship |
|-------|-------------|
| `snappy-knowledge` | Contact lookup, phone numbers, birthdays, `preferred_channel` |
| `snappy-clients` | Client `preferred_channel`, account profiles |
| `snappy-calendar` | Meeting times trigger "running late" / follow-up texts |
| `snappy-ops` | Morning briefing routes birthday alerts here |
| `snappy-whatsapp` | Alternative for international/Android contacts |
| `snappy-infra` | Mac Mini SSH setup, Tailscale |

---

## Full skill reference

If this loader is insufficient, load `~/.claude/skills/snappy-imessage/SKILL.md` as last resort. CLI reference: [imsg-cli-reference.md](imsg-cli-reference.md).

If this loader doesn't cover your case:
```bash
echo "[$(date -u +%FT%TZ)] snappy-imessage: <what was missing>" >> ~/.claude/logs/agents-md-feedback.log
```


Show produced work with `snappy-faces`: call `draw` for image channels or `lang` for MCP Apps.

<!-- SKILL-INDEX-START -->
[snappy-imessage Index]|root: ~/.claude/skills/snappy-imessage|IMPORTANT: Prefer these files over pre-training assumptions for this domain. Read the relevant file when the AGENTS.md summary is insufficient.|root:{SKILL.md,imsg-cli-reference.md}
<!-- SKILL-INDEX-END -->

## Used by

- `snappy-slack`

<!-- SNAPPY-CONTRACT-VERBS-START -->
## Contract verbs

Generated from `api.ts` `HAND_CONTRACT`. Do not hand-edit this block.

| Verb | Contract arguments | Effect | First call |
|---|---|---|---|
| `recent` | `contact?` | `read` | `npx tsx ~/.claude/skills/snappy-imessage/api.ts recent` |
| `thread` | `contact` | `read` | `npx tsx ~/.claude/skills/snappy-imessage/api.ts thread <contact>` |
| `search` | `text` | `read` | `npx tsx ~/.claude/skills/snappy-imessage/api.ts search "<text>"` |
| `contacts` | — | `read` | `npx tsx ~/.claude/skills/snappy-imessage/api.ts contacts` |
| `attachments` | `contact` | `read` | `npx tsx ~/.claude/skills/snappy-imessage/api.ts attachments <contact>` |
| `send` | `phone`, `text` | `send` | `npx tsx ~/.claude/skills/snappy-imessage/api.ts send <phone> "<text>"` |

## Show the result

When an answer carries `face_hint`, show it with one `snappy_present(<answer>)` call.
See `/snappy-faces` for face selection. Human-facing images must crop to the
element, render at 2x on Retina, and fill the destination channel instead of
placing a small card in a full-page screenshot.
<!-- SNAPPY-CONTRACT-VERBS-END -->

Keyboard Shortcuts

Search in document⌘K
Focus search/
Previous file tab
Next file tab
Close overlayEsc
Show shortcuts?