Hermes iMessage via Photon: gRPC, No Mac Relay, Two Commands to First Text

Hermes Agent v0.17.0 (release) shipped a Photon Spectrum plugin that lets any Hermes install send and receive iMessage through Photon's managed line pool. Every prior iMessage bridge - BlueBubbles, AirMessage, the previous Hermes iMessage path - required a Mac running 24/7 as a relay. v0.17.0 removes that constraint and replaces the prior webhook design with a gRPC-native persistent stream.
The plugin registers at plugins/platforms/photon/ and adds zero lines to gateway/run.py, cli.py, or any other core file. The only non-plugin change in the merge was a docs sidebar entry (PR #32348).
What Changed: Webhook to gRPC
PR #42444, also in v0.17.0, reworked the initial webhook design into a persistent-connection channel on par with the Discord and Slack adapters. The new shape holds a long-lived gRPC stream to Photon inside a small supervised Node sidecar (because spectrum-ts is TypeScript-only) and bridges into the Python adapter over loopback HTTP.
What the rework removed: the aiohttp webhook server, HMAC signature verification on inbound, the public-URL requirement, and every PHOTON_WEBHOOK_* config key.
What it added: a long-lived bidirectional gRPC stream, loopback HTTP into the Python adapter for both directions, bidirectional attachments (Hermes can see and send images and voice notes), automatic reconnect on network drop, and auto-configured allowlist, home channel, and cron channel on setup.
The Setup Flow
Onboarding collapses to two commands:
hermes photon setup --phone +155****4567
hermes gateway start --platform photon
The setup command runs device-code OAuth against app.photon.codes, finds or creates a Hermes Agent project, enables Spectrum, rotates the project secret, registers the phone number (with dedup so re-runs are safe), and prints the assigned iMessage line. Runtime credentials write to ~/.hermes/.env. Management metadata goes to ~/.hermes/auth.json.
| Step | What it does | Idempotent |
|---|---|---|
| Device login | RFC 8628 flow against photon-cli | One-time |
| Find or create project | Hermes Agent project on Photon | Yes |
| Enable Spectrum | Reads spectrumProjectId | Yes |
| Rotate secret | New project secret for SDK auth | No |
| Register user | Binds phone number | Yes |
| npm install sidecar | Pulls spectrum-ts | Yes |
Architecture: Why a Sidecar
spectrum-ts ships as TypeScript only. Hermes is Python. The plugin keeps spectrum-ts in a small Node process supervised by the adapter, talks loopback HTTP into the Python side, and runs the gRPC stream outside the Python process. The pattern mirrors how ui-tui/ handles its Node dependencies - Hermes does not need to be polyglot to consume a polyglot SDK.
Plugin breakdown: adapter.py ~500 lines (loopback HTTP, dispatch, hooks), auth.py ~400 lines (device OAuth, project lifecycle, secret rotation), cli.py ~300 lines (hermes photon setup / status / install-sidecar), Node sidecar/index.mjs ~250 lines (spectrum-ts gRPC supervisor), tests ~430 lines. Total plugin surface: ~1,880 lines, all under plugins/platforms/photon/ with zero edits to gateway/run.py or any other core file.
Zero edits to core. The plugin registers via ctx.register_platform() and ctx.register_cli_command() only.
Free vs Paid: What the Shared Pool Means
The default setup uses Photon's type: shared line pool, the free tier. Different recipients may see different sending numbers for outbound messages from your agent, but each conversation stays stable. Paid Business tier users get a dedicated number. The plugin supports both - the docs recommend starting on the free pool.
For bot-to-human messaging (notifications, support, reminders), the shared number is fine. For personal identity (Hermes texts as Ryan), upgrade to a dedicated line.
Comparison: Photon vs Prior Bridges
Every prior iMessage bridge shared the same constraint - somewhere in the chain a Mac had to be running. Photon is the first iMessage layer that does not require Apple hardware on the operator side.
| Bridge | Mac required | Connection model | Cost |
|---|---|---|---|
| BlueBubbles | Yes, always-on | Local push, custom protocol | Free + Mac |
| AirMessage | Yes, server or local | Custom protocol, port-forward | $4-$7/mo + Mac |
| Hermes (v0.16, pre-Photon) | Yes, via BlueBubbles bridge | Webhook + HMAC | Free + Mac |
| Hermes Photon (v0.17) | No | Persistent gRPC stream | Free / paid tier |
The technical shift is in the last column: from "free plus an always-on Mac" to "free without the hardware." The first column is the practical shift: anyone with a $5 VPS running Hermes can now reach people on iMessage.
What the Plugin Demonstrates
Three things matter here:
- Polyglot SDKs work without polyglot runtimes. A TypeScript-only SDK sits behind a 250-line Node sidecar supervised from Python. Reusable for any future channel that ships SDK-first.
- Webhooks are not the only shape for inbound messaging. A persistent bidirectional gRPC stream is strictly better when the upstream offers it. Latency drops, retry semantics simplify, attachments go both directions.
- Plugin-isolated platforms keep core stable. Every other platform adapter (Telegram, Discord, Slack, Signal, WhatsApp) lives in
plugins/platforms/. v0.17.0 added Photon with zero growth in the size ofgateway/run.py.
If you run Hermes on a Linux VPS and want iMessage access, the free tier costs nothing to try. Two commands, blue bubbles in minutes. The shared-number tradeoff is real for personal identity but a non-issue for bot-to-human or automation traffic.
[^1]: Tony reviews things. "Hermes Agent iMessage via Photon Spectrum (No Mac Needed)". June 20, 2026. [^2]: NousResearch/hermes-agent. "Photon iMessage docs". June 2026. [^3]: PR #42444. "feat(photon): gRPC-native iMessage channel (persistent connection, no webhook)". Merged into v0.17.0. [^4]: PR #32348. "feat(gateway): add Photon Spectrum (iMessage) platform plugin". Merged into v0.17.0. [^5]: v0.17.0 release notes. "The Reach Release". June 19, 2026.