Build log

I put an AI agent in my text messages.

Not an app. Not a tab. I text a thread on my phone and something on my Mac reads it, decides what to do, does it, and texts back. It has 32 skills, two model providers, and four jobs it runs without being asked. The hardest bug was stopping it from talking to itself forever.

Why

The interface already existed

Every assistant wants to be an app you open. But the thing I actually have open all day is iMessage, and it already syncs to my watch, my laptop and my phone with no work from me. So instead of building a client, I made the agent a contact.

The bar was simple: it should be faster to text it than to do the thing myself. Anything slower than that and I'd quietly stop using it, which is how most personal tooling dies.

Architecture

How it's wired

A long-running gateway process on a Mac mini, a message bridge into iMessage, and a model router in front of two providers.

The gateway

A Node process that stays up and owns the session state. It binds to loopback only and requires a token, so nothing off the machine can reach it. Sessions are scoped per channel and peer, which keeps separate conversations from bleeding context into each other.

The message bridge

A CLI that reads and sends through the local Messages database. Inbound is allowlisted to specific senders. Group threads are off entirely, and the agent is not allowed to rewrite its own config from a chat message. If someone gets into the thread, they get an assistant, not a shell.

Two providers, one router

Primary is GPT-5.5 running on the codex agent runtime. Fallback is Claude Sonnet 4.6. This isn't hedging on quality, it's hedging on availability: hit a usage limit mid-errand with a single provider and the assistant is just down. With a fallback it degrades instead of dying.

32 skills

Discrete capabilities it can reach for: password vault, notes, transcription, calendar, Trello, Spotify and Sonos, Hue, screenshots, PDF tools, tmux, web search. Each is a bounded tool rather than a general "run this on my computer," which is the difference between a useful assistant and a loaded gun.

Persistent memory

A memory plugin backed by a local database, so context survives restarts. Without it every conversation starts from zero and you end up re-explaining your own life to your assistant daily.

Four scheduled jobs

It isn't only reactive. Four cron jobs fire on their own, all pinned to America/New_York so daylight saving doesn't silently shift them by an hour twice a year.

JobScheduleWhat it's for
site-deploy-watchdogDaily 08:15Did last night's site build actually deploy?
weekly-spending-recapMondays 08:00Reads the inbox, texts me what I spent
affiliate-commission-recapMondays 08:05RunRace USA affiliate earnings
oauth-keepaliveMondays 09:00Refreshes tokens before they quietly expire
The bug I'm proudest of catching

It talked to itself forever

The thread I use is a self-chat: I text myself, and the agent reads that thread. Which creates a problem that doesn't exist in any normal chat bot. When the agent replies, its reply lands in the same thread it's watching. It reads its own message as new user input, answers that, reads the answer, and you have an infinite loop burning tokens at machine speed.

The fix is almost stupid: every outbound message is prefixed with a lobster emoji, and the inbound reader drops anything carrying that prefix. One character of state, encoded in the message itself, which means it survives restarts and needs no bookkeeping.

The general lesson is the one worth keeping. Any agent that writes into a channel it also reads needs a way to recognise its own voice. Get that wrong and the failure isn't subtle: it's a runaway.

What else broke

The unglamorous half

16 crash reports sit on disk from the last few months, each written out as structured JSON rather than vanishing with the process. Being able to read what killed it is most of the work.

macOS permissions reset themselves

Reading the Messages database needs Full Disk Access, and that grant is tied to the exact binary. Upgrade Node through nvm and the path changes, so the permission silently no longer applies. The agent comes up fine and simply stops seeing messages. Now every Node upgrade means re-granting access, on purpose.

Assertion failures took the process down

The recurring crash class was uncaught assertions and unhandled promise rejections, both of which kill a Node process outright. A long-running assistant needs a supervisor and a crash log, because it will go down and you need to find out from a log rather than from texting it and getting silence.

Threaded replies don't send

Reply-in-thread from the Messages UI doesn't make it through the bridge. Still open. It's listed here rather than quietly omitted because a build log with no open bugs is a brochure.

Takeaways

What I'd tell someone building one

Meet people where they already are

The reason I actually use this is that it costs nothing to reach. No new app, no new habit. The best interface is usually one already in your hand.

Bound the blast radius early

Allowlisted senders, no groups, no config writes from chat, named skills instead of arbitrary execution. An agent with your credentials and your shell is a security surface, and it deserves to be treated like one from day one.

Failover is a feature, not paranoia

One provider means one outage away from useless. Two providers and a router turned "it's down" into "it's slower today."

Scheduled beats reactive

The jobs I value most are the ones I never trigger. A Monday text telling me what I spent is worth more than an assistant waiting to be asked, because I'd never remember to ask.

Running on a Mac mini at home right now. Security details are described at the level of posture rather than configuration, deliberately. Back to all build logs.