A Linq iMessage gateway adapter for Hermes Agent. Send and receive real iMessage "blue bubbles" through the hosted Linq partner API — no Mac and no BlueBubbles server required.
This is the Hermes counterpart to the OpenClaw Linq channel plugin: same Linq Blue v3 API, ported to Hermes' Python gateway/platform plugin system. It sits alongside Hermes' bundled BlueBubbles and Photon iMessage channels as a third independent way to reach iMessage.
| BlueBubbles | Photon | Linq (this plugin) | |
|---|---|---|---|
| Requires a Mac | ✅ yes | ❌ no | ❌ no |
| Outbound transport | BlueBubbles REST | Node spectrum-ts sidecar |
direct Linq REST |
| Inbound transport | webhook | signed webhook | signed webhook |
| Extra runtime deps | BlueBubbles server | Node 18+ | none beyond Hermes |
Because Linq exposes a public send endpoint, this adapter needs no Node
sidecar — every outbound message, typing indicator, and read receipt is a
direct httpx call.
Copy this directory to ~/.hermes/plugins/linq/ (or a project-local
.hermes/plugins/linq/), then enable it:
hermes plugins enable linq-platformThe webhook listener needs aiohttp, which is an optional Hermes
dependency (it ships with the hermes-agent[messaging] extra, not the
core install). If hermes linq status reports a missing dependency:
pip install aiohttppip install hermes-linq-plugin
hermes plugins enable linq-platformHermes discovers it through the hermes_agent.plugins entry point.
Run the one-time setup (stores your token + from-phone in ~/.hermes/auth.json):
hermes linq setupYou'll be asked for:
- Linq API token — from your linqapp.com dashboard
(or set
LINQ_API_TOKENin your environment instead). - From-phone (optional) — the Linq number this agent sends from, in E.164
(
+15551234567). Only needed to pin a multi-number account to one line.
Linq also surfaces in the unified wizard:
hermes gateway setup # pick "Linq iMessage"Linq delivers inbound iMessages as webhooks. Point your Linq dashboard at the gateway's public URL:
hermes linq webhook show --public-url https://your-public-host
# → register https://your-public-host/linq/webhook in the Linq dashboardThen export the signing secret the dashboard gives you so deliveries are verified:
export LINQ_WEBHOOK_SECRET=<secret-from-linq-dashboard>hermes gateway start --platform linqVerify any time with:
hermes linq status # credential state + live connectivity probeEverything is configurable by environment variable, by ~/.hermes/config.yaml,
or in ~/.hermes/auth.json. Precedence is env → config.yaml → auth.json.
platforms:
linq:
enabled: true
extra:
from_phone: "+15551234567"
webhook_port: 8790
webhook_path: /linq/webhook
send_read_receipts: true
require_mention: false
mention_patterns:
- '(?<![\w@])@?hermes\b[,:\-]?'| Variable | Default | Purpose |
|---|---|---|
LINQ_API_TOKEN |
— | Required. Linq partner API bearer token |
LINQ_FROM_PHONE |
— | Pin a multi-number account to one Linq line (E.164) |
LINQ_WEBHOOK_SECRET |
— | HMAC-SHA256 secret for inbound webhook verification |
LINQ_WEBHOOK_PORT |
8790 |
Local webhook listener port |
LINQ_WEBHOOK_PATH |
/linq/webhook |
Local webhook listener path |
LINQ_WEBHOOK_BIND |
0.0.0.0 |
Webhook listener bind address |
LINQ_API_BASE |
https://api.linqapp.com/api/partner/v3 |
Linq API base URL |
LINQ_SEND_READ_RECEIPTS |
true |
Send read receipts + typing on inbound |
LINQ_ALLOWED_USERS |
— | Comma-separated allowlist of E.164 senders |
LINQ_ALLOW_ALL_USERS |
false |
Allow any sender (dev only) |
LINQ_REQUIRE_MENTION |
false |
Gate group chats on a wake word |
LINQ_MENTION_PATTERNS |
Hermes wake words | Group mention regexes |
LINQ_HOME_CHANNEL |
— | Default chat id for cron / notification delivery |
The gateway denies unknown senders by default. Authorize them one of two ways (same model as every Hermes platform):
- DM pairing — a new sender gets a code; approve it:
hermes pairing approve linq <CODE>
- Pre-authorize —
export LINQ_ALLOWED_USERS=+15551234567,+15557654321
Set LINQ_ALLOW_ALL_USERS=true only for local development.
- Real iMessage blue bubbles via the Linq API — no Mac
- Inbound via HMAC-SHA256 signed webhooks (replay-protected, 5-min drift window)
- At-least-once delivery dedup on
message.id - Outbound text + media-by-URL, typing indicators, read receipts
- Inbound image attachments downloaded locally for the vision tools
- Group-chat mention gating (parity with the Photon / BlueBubbles channels)
- E.164 phone numbers treated as PII and redacted before reaching the LLM
- Cron /
send_messagedelivery, including out-of-process (standalone_sender) - Unified
hermes gateway setuponboarding + a dedicatedhermes linqCLI
Inbound: iMessage → Linq → signed webhook → aiohttp listener → MessageEvent → agent
Outbound: agent → LinqClient (httpx) → Linq REST API → iMessage
adapter.py—LinqAdapter(BasePlatformAdapter), the webhook server, and theregister(ctx)plugin entry point.linq_api.py— async Linq REST client (send / typing / read / reaction / probe).signing.py— dependency-free webhook-signature verification, mention gating, and payload parsing (unit-tested in isolation).auth.py— credential storage in~/.hermes/auth.json.cli.py—hermes linq {setup,status,probe,webhook show}.
The security-critical and parsing logic lives in signing.py and auth.py,
which import nothing from Hermes, so the test suite runs anywhere:
python -m unittest discover -s tests -vadapter.py, cli.py, and linq_api.py import the host gateway.* package
and Hermes' BasePlatformAdapter, so they exercise fully only inside a Hermes
runtime.
- Group detection. Linq's documented
message.receivedpayload (mirrored from the OpenClaw plugin) does not include a first-class chat-type field, sosigning.is_group_chat()infers group vs. DM fromis_group/group_id/participants. Confirm against a real Linq group webhook and tighten if Linq exposes an explicit type. - Outbound media. Linq sends media by public URL, not multipart upload,
so
send_imageforwards a URL and the standalone/cron path skips local files with a logged note. If your Linq plan offers an upload endpoint, wire it intoLinqClient.send_message. - Webhook signature. Verification matches the OpenClaw plugin's scheme
(
hex(hmac_sha256(secret, "{timestamp}.{body}")),X-Webhook-Timestamp/X-Webhook-Signature). Adjustsigning.verify_signatureif your dashboard documents a different header layout.
MIT — see LICENSE.