How to Run an iMessage Agent Without a Mac
Run an iMessage agent without a Mac: no Mac mini to keep awake, no Apple ID to babysit, and your stack stays on Linux. Build it and text it in 30 minutes.

Short answer: You can run an iMessage agent without a Mac by putting it on a hosted iMessage line instead of a Mac you sign in, keep awake and script yourself. On Keiki you write the agent in the dashboard or in TypeScript, add a dedicated iMessage line, and Keiki runs delivery, memory and approvals, so nothing in your stack needs macOS.
iMessage only runs on Apple hardware, and your servers almost certainly don't. So the first thing most teams try is a Mac mini in a closet: sign it into an Apple ID, run a script that reads the Messages app, and point the agent at it. It works for a demo. Then the Mac installs an update at 3 a.m., signs itself out, and your agent goes quiet until someone walks over and logs back in.
This guide shows how to build and run an iMessage agent without a Mac, so the agent lives on a real phone line and your own stack can stay on Linux. You'll learn:
- Why iMessage seems to need a Mac in the first place
- What breaks when a Mac runs your agent
- The five steps to launch an iMessage agent without one
- Which iMessage features still work, and what to check before you go live
This is one of the step-by-step guides in Building iMessage agents, our complete guide to building iMessage agents.
Why iMessage seems to need a Mac
On Slack, Telegram or WhatsApp you register a bot, get a token and receive webhooks. iMessage has no equivalent. Apple doesn't offer a public API for sending and receiving iMessages from an ordinary phone number on a server, and the Messages app only runs on Apple devices.
So the do-it-yourself route is a bridge: a Mac signed in to an Apple ID, a script or bridge app that watches the Messages database for new texts, and AppleScript to send replies. Your agent server talks to the Mac over the network. That is a fine way to text yourself reminders. It is a hard way to run something customers depend on.
What breaks when a Mac runs your agent
- It goes offline. Macs sleep, restart for updates, lose Wi-Fi and get signed out of iCloud. Each one is an outage, and nothing fails over.
- One Mac, one Apple ID. Every customer texts the same account. More traffic or a second number means another machine to buy, sign in and watch.
- The bridge is the fragile part. Reading the Messages database and scripting the Messages app depend on macOS internals that can change with any update.
- Your network is now part of the product. The agent server has to reach a Mac on a home or office network, and that Mac holds every customer conversation on its disk.
- Everything else is still yours. The bridge only moves messages. The agent loop, memory, retries, a burst of three texts in a row, approvals and logs are all still to build.
What you need instead
An iMessage agent needs three things, and none of them is a Mac:
- An iMessage line that someone else keeps signed in, online and delivering.
- An agent: instructions, a model and tools that call your APIs.
- A runtime that receives each message, runs the agent, keeps memory, asks a person before risky actions and delivers the reply.
Keiki provides the line and the runtime. You write the agent from any machine, including a Linux laptop or a CI job, and your APIs stay where they are.
How to build an iMessage agent without a Mac
You'll need a Keiki account, an API key from Settings → API keys, and Bun if you write the agent in TypeScript. You can also do steps 1 and 2 entirely in the dashboard.
Step 1: Describe the agent
Create an agent and write its instructions the way you'd brief a new hire. In TypeScript:
// src/agent.ts
import { create } from '@onkeiki/agents'
const kit = create({
apiKey: process.env.KEIKI_API_KEY!,
name: 'Acme Support',
agent: {
model: 'google/gemini-3.5-flash',
maxSteps: 10,
historyLimit: 40,
systemPrompt: ({ now }) => [
'You are the support agent for Acme, texting customers over iMessage.',
'Keep replies short, like a text from a helpful person.',
'Hand off to a human when a customer asks for one.',
`Current time: ${now.toISOString()}`,
].join('\n'),
tools: [],
},
})
await kit.sync()
Or start from an agent template and edit it in the dashboard.
Step 2: Give it tools
A tool is one of your API calls with a name, a description and an input schema. A phone number is not a login, so gate account reads on identity: your identity webhook returns scopes only for a sender it has matched to a customer, and Keiki refuses a tool whose requiredScopes that sender lacks. Then scope the call to the person texting (ctx.phone), never only to an ID the model passes in, so one customer can't read another's order.
// src/tools.ts
import { Effect, Schema } from 'effect'
import { defineTool } from '@onkeiki/agents'
export const lookupOrder = defineTool({
name: 'lookup_order',
description: 'Look up an order by its number. Returns status, items, and delivery date.',
input: Schema.Struct({ orderNumber: Schema.String }),
policy: { requiredScopes: ['orders:read'] },
execute: (input, ctx) =>
Effect.promise(async () => {
const customer = encodeURIComponent(ctx.phone)
const res = await fetch(`https://api.acme.com/customers/${customer}/orders/${encodeURIComponent(input.orderNumber)}`, {
headers: { Authorization: `Bearer ${process.env.ACME_API_KEY}` },
})
return await res.json()
}),
})
In src/agent.ts, add import { lookupOrder } from './tools' and set tools: [lookupOrder]. Keep kit.sync() in src/agent.ts and the tool in its own exported module, so the hosted bundle can load the tool without re-running the sync. Already run an MCP server? Connect it from the agent's tools in the dashboard instead. For anything that changes money or data, set policy: { approval: 'ask' } so a person decides before it runs. See governed actions.
Step 3: Sync from any machine
bun add @onkeiki/agents effect
bun run src/agent.ts
kit.sync() pushes the config and tools to Keiki. It runs anywhere Bun runs, so Linux, a container or your CI pipeline all work. From here Keiki runs the agent loop, delivery, memory and approvals. You don't run a server for the agent, and nothing needs macOS.
Step 4: Add an iMessage line
In the dashboard, open the agent's Channels and add an iMessage line. Buy a dedicated line in Keiki, or import a line you already have. Keiki keeps the line online and delivering, and tracks a receipt for every message the agent sends: sent, delivered, read or failed. See cloud agents for every channel.
Step 5: Text it and read the trace
Text the number from your iPhone. Ask about an order, then text the way customers really do: three messages in a row, a typo, "actually, make it Saturday." Keiki groups a burst into one turn and picks up new messages mid-answer.
Open the conversation's trace. It shows what the agent read, which tools it called with which arguments, and what it cost. If an answer is wrong, the trace tells you whether the prompt, the tool or your API caused it.
What still works without a Mac
Moving off a Mac doesn't mean giving up what makes iMessage feel native:
- Tapbacks, threaded replies and effects work on every iMessage line, imported lines included. The agent sees each message's id, so it can react to a message or answer it directly.
- Photos with context. A photo and the text sent with it arrive as one message, and attachments are stored so your tools can fetch the original file.
- Polls and voice notes that play in a bubble.
- App cards. With the iMessage Apps add-on on a dedicated line bought in Keiki, a tool can open a booking page or checkout as a card in the thread, with no iOS app required.
Already built your own agent?
Keep it. With the external harness, your model loop runs in your own app, on your own Linux servers, while Keiki receives iMessages, delivers replies and records every turn. If you're starting from your product's endpoints instead, follow how to turn your API into a WhatsApp and iMessage agent.
Before you go live
- Test the real jobs. Text the agent the five things customers ask most, and check each tool call in the trace.
- Know who's texting. A phone number is not a login. Set up identity before the agent reads or changes someone's account.
- Keep writes on ask. Leave every tool that changes data on
approval: 'ask'for the first week, then loosen it one tool at a time. - Plan the handoff. Decide which conversations go to a person, and make sure that person gets the whole thread.
FAQ
Do I need a Mac to run an iMessage agent? No. A Mac is only required if you host the iMessage account yourself. On Keiki the agent runs on a hosted iMessage line, and you build it from the dashboard or any machine that runs Bun.
Can I run an iMessage bot on Linux or Windows? Yes. You write and sync the agent from Linux, Windows or any machine that runs Bun, and your APIs and tools stay on your own servers. Keiki runs the iMessage line and the agent loop, so nothing in your stack needs macOS.
Can I keep the phone number I already use? You can import a line you already have from Channels, or buy a dedicated line in Keiki. App cards need a dedicated line bought in Keiki.
Do my customers need to install anything? No. They text a phone number from the Messages app they already use.
How long does it take? Describing the agent, connecting a tool and adding a line takes under 30 minutes. Testing it against real conversations is where the rest of your time should go.
Run your iMessage agent without a Mac
Start building on Keiki, read the developer docs, or begin from an agent template.