A macOS CLI that bridges Apple Contacts and iMessage for terminal users and AI agents.
kith resolves a name to a person to a chat. The killer flow:
kith history --with "Mark Kropf"…walks CNContactStore to find Mark, collects all his phones (E.164) and emails,
joins them against ~/Library/Messages/chat.db to find the canonical 1:1
conversation, and streams messages newest-first. Group chats and named threads
are auto-excluded so "messages with Mark" actually means messages with Mark.
Read-only. macOS 14+. arm64.
If you've been wiring iMessage into agents on macOS, you're probably already using Peter Steinberger's imsg. It solves the messaging primitive cleanly (read and send, both directions) and is MIT-licensed.
kith vendors imsg's MessagesCore as its base layer (see THIRD_PARTY_NOTICES.md) and adds the layer that was missing for me on the read side: contact resolution.
The shape of the difference:
- imsg accepts handles directly: phone numbers, emails, chat IDs. It can also send.
- kith starts with a name, walks
CNContactStore, collects the contact's full set of phones (E.164) and emails, and joins them against~/Library/Messages/chat.dbto land on the canonical 1:1 conversation. Stable across renames and contact merges, because the resolution is anchored on the stableCNContactUUID, not on a phone number or display name.
Same underlying message database. Apple Contacts joined on top. Read-only by design today; sending stays imsg's job.
brew tap supaku/tools
brew install --cask kithPulls the signed + notarized arm64 binary from the latest GitHub Release. The cask is auto-bumped on every tag push. See RELEASING.md for how that works.
If you'd rather compile locally (useful on machines without an arm64 Homebrew, or for development), Formula/kith.rb ships a build-from-source formula:
brew tap supaku/kith https://github.com/supaku/kith
brew install kithgit clone https://github.com/supaku/kith.git
cd kith
swift build -c release
cp .build/release/kith ~/bin/ # or anywhere on $PATHOr use the bundled script. It stamps BuildInfo.swift with the current commit
SHA + ISO build timestamp:
scripts/build.shTo codesign with a Developer ID Application certificate (hardened runtime):
KITH_SIGN_IDENTITY="Developer ID Application: Your Name (TEAMID)" scripts/build.shkith needs two macOS permission grants. Run kith doctor first. It tells
you exactly what's missing and how to fix it.
Granted via the standard TCC prompt the first time you run kith find. If your
terminal isn't pre-listed in System Settings → Privacy & Security → Contacts:
- Click
+, navigate to your terminal app (Ghostty, iTerm, Terminal, etc.), add it. - For Electron apps (VS Code), you must
+-add manually since they don't declareNSContactsUsageDescription. - Cmd+Q to fully quit your terminal, then relaunch. TCC grants only inherit on a fresh process tree.
Required to read ~/Library/Messages/chat.db. There is no programmatic prompt:
- System Settings → Privacy & Security → Full Disk Access.
- Add your terminal (not the
kithbinary itself; FDA is inherited by child processes). - Restart the terminal.
If kith doctor --json reports permissions.fullDiskAccess.status: "denied",
the most common fix is the quit-and-relaunch step. Toggling the switch while
the terminal is running won't take effect.
kith find --name "Mark"
kith find --email "@acme.com"
kith find --phone "(415) 555-1212"
kith find --org "Rensei" --jsonlkith get "Mark Kropf"
kith get 0AB81E1A-DEAD-BEEF-CAFE-000000000001 --jsonkith chats --limit 20
kith chats --with "Mark Kropf" # cross-domain: name → all chats
kith chats --participant "+14155551212" # by handlekith history --with "Mark Kropf" # canonical 1:1 (default)
kith history --with chat-id:158 # explicit chat
kith history --with "Mark Kropf" --limit 200 --jsonl
kith history --with "Mark Kropf" --start 2026-01-01T00:00:00Z
kith history --with "Mark Kropf" --inline # render images in supported terminals
kith history --with "Mark Kropf" --raw-text # skip the U+FFFC/U+FFFD/U+0000 cleanup
kith history --with "Mark Kropf" --include-reactions
kith history --with "Mark Kropf" --attachments # add metadata array per message--with accepts: name | phone | email | chat-id:<n> | chat-guid:<g> | <CNContact-uuid>.
Chat-id and chat-guid forms require the prefix; bare integers are never
interpreted as chat IDs. Multiple chat-ids can be unioned via
--with chat-id:1,4,7.
The default behavior auto-prefers the canonical 1:1 conversation (chat where
chat_identifier matches an identity, no display_name, exactly one other
participant). Group chats and named threads are excluded. When the resolution
spans multiple 1:1 shards (chat-id rotation), they're unioned silently. When
only group chats match, kith exits 4 with the candidate list so you can pick
explicitly.
kith doctor # human report
kith doctor --json # machine-readablekith tools manifest --style kith # native shape (source of truth)
kith tools manifest --style anthropic # input_schema-style tools array
kith tools manifest --style openai # function-tool array
kith tools manifest --style json-schema # full JSON Schema 2020-12
kith tools schema --type Message # any of: Contact, ContactGroup, Chat,
# Message, Handle, Attachment, Error,
# DoctorReport
kith tools help # full command surface in one stream| flag | applies to | format |
|---|---|---|
| (none) | all commands | TTY-friendly human output, ANSI-styled |
--json |
single-record commands (get, doctor, version) |
one JSON object |
--jsonl |
streaming commands (find, chats, history, groups …) |
newline-delimited objects |
Errors emit a JSON envelope to stderr in machine mode (code, exit,
message, hint, candidates); in human mode, a single-line error + indented
hint + a candidate list when relevant.
Color is automatic when stdout is a TTY. Override via --color {auto,always,never},
KITH_COLOR=always|never, NO_COLOR, or CLICOLOR_FORCE=1 env vars.
kith history --inline renders attachment images directly in the terminal
when one of these is detected:
- iTerm2 (
TERM_PROGRAM=iTerm.app): iTerm2 inline image protocol. - VS Code's integrated terminal (
TERM_PROGRAM=vscode): same protocol. - WezTerm (
TERM_PROGRAM=WezTerm): same protocol. - Ghostty (any of
GHOSTTY_RESOURCES_DIR,GHOSTTY_BIN_DIR,GHOSTTY_VERSION,TERM_PROGRAM=ghostty): Kitty graphics protocol. - Kitty / KITTY_WINDOW_ID set: Kitty graphics protocol.
If none match, --inline silently falls back to [attachment: <name>] text.
Force the protocol with KITH_INLINE_PROTOCOL=kitty|iterm2|none (helpful inside
tmux, where outer-terminal env vars sometimes get masked). Set KITH_DEBUG=1
to see why a particular attachment didn't render.
HEIC / HEIF / WEBP attachments are converted to PNG via /usr/bin/sips before
transmission. Animated GIFs render as a still on Kitty-protocol terminals.
--inline is mutually exclusive with --jsonl.
| code | meaning |
|---|---|
| 0 | OK |
| 1 | GENERIC_ERROR |
| 2 | USAGE |
| 3 | NOT_FOUND |
| 4 | AMBIGUOUS_MATCH (caller must disambiguate) |
| 5 | PERMISSION_DENIED (TCC: Contacts or FDA) |
| 6 | DB_UNAVAILABLE |
| 7 | INVALID_INPUT |
kith is built so an LLM agent can ingest the entire CLI surface with one
tool call:
# Native shape. Ideal for an in-house tool registry.
kith tools manifest --style kith
# Drop straight into an Anthropic Messages API tools[] array.
kith tools manifest --style anthropic
# OpenAI tool-calling shape.
kith tools manifest --style openai
# Or, if your agent prefers reading help text rather than JSON schemas:
kith tools help # 200-ish lines, every command + flagFor BI/observability use cases, the typical recipe is:
kith doctor --jsononce, gate onok: true.kith find --name "<query>" --jsonl --limit 5to surface candidate contacts.kith get "<exact-full-name>" --resolve-only --jsonto lock inid+fullName(the CNContact UUID is stable; a future-proof handle).kith history --with <id> --jsonl --limit 200to stream messages.
Pin the contact UUID in your records. It survives renames and contact merges better than a phone number.
| path | what |
|---|---|
Sources/kith |
the executable + manifest projections |
Sources/ContactsCore |
CNContactStore wrapper + Contact/ContactGroup models |
Sources/MessagesCore |
vendored from imsg (MIT); sync via scripts/vendor-sync.sh |
Sources/ResolveCore |
the --with parser + cross-domain Resolver |
Tests/*Tests |
Swift Testing test suites |
Formula/kith.rb |
Homebrew formula (build-from-source) |
.github/workflows/ |
CI + release |
.claude/PLAN.md |
v1 architecture plan (single source of truth) |
THIRD_PARTY_NOTICES.md carries the MIT attribution + full license text for
vendored sources.
swift test79+ Swift Testing tests across 4 targets. The non-CN-dependent suites use
in-memory SQLite fixtures; kithTests shells the built binary against an
on-disk fixture DB via KITH_DB_PATH. None of the tests require real Contacts
or FDA grants.
MIT. See LICENSE. Vendored MessagesCore is also MIT (Peter Steinberger);
see THIRD_PARTY_NOTICES.md for attribution.
A Supaku Labs project.