Generic JSON-file-based bridge adapter for the Hermes Agent Gateway.
Instead of each messaging platform connecting directly to the Gateway, the Bridge Adapter provides a shared JSON-file interface — any external service can communicate with Hermes by reading/writing JSON files in a well-defined directory structure.
Terminology: an External Service is the messaging platform itself (your messaging service, a chatbot, …). An External Service Wrapper (or just wrapper) is the script that binds that platform to the JSON-file contract — it reads outbox/, writes inbox/ and status/, and copies attachments. The adapter never talks to the platform directly; it only exchanges JSON files with the wrapper.
┌─────────────────────┐ JSON files ┌──────────────────┐
│ External Service │ ┌──────────────┐ │ Hermes Gateway │
│ Wrapper │──▶│ inbox/ │──▶│ Bridge Adapter │
│ (your messaging │ │ <bridge>/ │ │ polls inbox/ │
│ service, chatbot) │◀──│ outbox/ │◀──│ writes outbox/ │
│ │ │ <bridge>/ │ │ dispatches │
│ │ │ status/ │ │ MessageEvents │
│ │ │ <bridge>/ │ │ │
│ │ │ media/ │ │ │
└─────────────────────┘ └──────────────┘ └──────────────────┘
<bridge_dir>/
├── registry/<bridge>.yaml ← bridge manifest (presence = registered)
├── unified_threads.json ← persisted unified threads (T-058)
├── reply_map.json ← gateway→local message-id map for cross-bridge reply chains (T-060)
├── identity_map.json ← alias→person map for member dedup (T-062)
├── active_threads.json ← per-person active thread (T-064)
├── paused_threads.json ← per-person paused routing (T-068)
├── pending_claims.json ← in-flight identity-claim challenges (T-065)
├── usernames.json ← per-person display name (T-065)
├── protokoll/<thread>/ ← rendered protokoll artifacts (T-059)
├── inbox/<bridge>/ ← written by external service wrapper, read by adapter
├── outbox/<bridge>/ ← written by adapter, read by external service wrapper
├── status/<bridge>/ ← written by external service wrapper, read by adapter
└── media/
├── <bridge>/incoming/ ← incoming attachments (wrapper → adapter)
└── <bridge>/outgoing/ ← outgoing attachments (adapter → wrapper)
Bridges register with the adapter by dropping a manifest into registry/:
# registry/imsg.yaml
name: imsg
service: imessage
host: mac-mini-01
target_format: [email, phone, chat_id]
capabilities: [text]- Presence of the manifest = the bridge is registered. The adapter
polls
registry/every few seconds and reconciles:- a new manifest → the bridge is registered and its
inbox/,outbox/,status/,media/directories are created; - the manifest is removed (
rm registry/imsg.yaml) → the bridge is deregistered and itsstatus//media/directories are cleaned up.
- a new manifest → the bridge is registered and its
- No config edit needed to add a bridge — just drop in a manifest (or remove it to take the bridge down). The adapter picks up changes at runtime without a restart.
target_formatdeclares which target shapes the bridge accepts (email,phone,chat_id). It is the basis for the routing check that decides whether a target is routable on a bridge.- A bridge that is registered (manifest present) may still be
disconnected (
status/<bridge>/status.jsonsaysconnected: false); the two are independent.
{
"id": "msg_abc123",
"type": "message",
"timestamp": "2026-07-26T12:00:00Z",
"from": {
"id": "user_42",
"name": "Alice",
"platform": "imsg"
},
"chat": {
"id": "chat_99",
"name": "Project Chat"
},
"text": "Hello! Can you check the deployment status?",
"thread_id": "thread_001",
"thread_root": "msg_001",
"attachments": [
{
"type": "image",
"path": "media/imsg/incoming/screenshot.jpg",
"mime": "image/jpeg"
}
],
"reply_to": {
"id": "msg_001",
"text": "Previous message"
}
}{
"id": "out_xyz789",
"type": "message",
"target": {
"chat_id": "chat_99",
"bridge": "imsg"
},
"text": "Sure, let me check that.",
"thread_id": "thread_001",
"attachments": [
{
"type": "image",
"path": "media/imsg/outgoing/result.png",
"mime": "image/png"
}
],
"typing": true,
"reply_to": {
"id": "msg_abc123"
}
}{
"connected": true,
"last_seen": "2026-07-26T12:00:00Z",
"error": null,
"version": "1.0.0"
}- Platform-agnostic — any service that reads/writes JSON can be a bridge
- Attachment support — images, files, documents via shared media directory
- Reactions — 👍 reactions on messages
- Typing indicators — show when Hermes is typing
- Reply chains — reply_to preserves conversation context
- Per-bridge config — mention patterns, user allowlists, poll intervals
- Auto-cleanup — old media files and stale outbox entries are purged
- Health monitoring — status files polled every 60s, logged on disconnect
- Registry self-registration (T-050) — bridges register via
registry/manifests, picked up at runtime without a restart - Agent awareness (T-051) — a system-prompt platform hint teaches the agent to read
registry/and address messages as<bridge>~<target>; every inbound message carries a compact routing line ([Message from <sender>, bridge <bridge>, reply to <bridge>~<target>]) - Routing fallback (T-053) —
send()validates the target; unroutable targets (unknown bridge / wrong format) return a clearSendResulterror instead of silently misrouting - Unified Threads (T-058–T-068) — the differentiator. Because every bridge is wired to the adapter through the same JSON-file contract, multiple bridges can share a single agent session — one conversation across iMessage, Talk, and any other wrapper. Native gateway adapters are isolated from each other; the Bridge Adapter turns them into one thread.
/unifiedcommands (with/ualias shortcuts), 5 participant modes, cross-bridge reply chains, adaptive bundling, member dedup, message relay that mirrors every inbound message to the other member bridges so all humans see the full conversation, plus active-thread switching, one-shot send (T-064), identity-claim challenge-response to authorize a user-mapping across bridges (T-065), unified handles (T-066) that decouple the on-thread identity from the raw bridge identity (unified~<username>, agent handle from config), and exit (T-067/T-068) to pause a member chat back into the normal per-bridge DM. → Full docs:UNIFIED_THREADS.md
-
Install the plugin:
cp -r bridge-adapter ~/.hermes/plugins/ -
Enable in
~/.hermes/config.yaml:plugins: enabled: - bridge-adapter gateway: platforms: bridge-adapter: enabled: true
-
Set environment:
export BRIDGE_DIR=/home/user/.hermes/bridge -
Restart the Gateway.
Bridges self-register by dropping a manifest into registry/. The adapter polls registry/ every few seconds and picks up new/removed manifests at runtime — no config change, no restart.
To add a new bridge (e.g. your messaging service):
# 1. Write a registry manifest (this is the registration)
cat > <bridge_dir>/registry/myservice.yaml <<'EOF'
name: myservice
service: my-messaging-service
host: my-host
target_format: [chat_id]
capabilities: [text]
EOF
# 2. The adapter creates inbox/outbox/status/media/myservice automatically
# 3. Write a wrapper script that:
# - Reads outbox/myservice/*.json → sends via your service's API
# - Writes incoming messages to inbox/myservice/*.json
# - Writes status to status/myservice/status.json
# 4. Start the wrapper
python3 myservice-wrapper.pyTo take a bridge down, remove its manifest (rm registry/myservice.yaml) — the adapter deregisters it and cleans up. See wrappers/imsg-wrapper.py for a complete wrapper example.
<bridge_dir>/
├── registry/
│ ├── imsg.yaml ← iMessage bridge manifest
│ └── myservice.yaml ← your messaging service bridge manifest
├── inbox/
│ ├── imsg/ ← iMessage wrapper writes here
│ └── myservice/ ← your messaging service wrapper writes here
├── outbox/
│ ├── imsg/ ← Adapter writes iMessage replies here
│ └── myservice/ ← Adapter writes your messaging service replies here
├── status/
│ ├── imsg/
│ └── myservice/
└── media/
├── imsg/
└── myservice/
Each bridge is fully isolated — its own inbox, outbox, status, and media directories. Wrappers only touch their own namespace.
A bridge wrapper is any script that:
- Reads JSON files from
outbox/<bridge>/(messages from Hermes) - Sends them via the platform's API (your messaging service, chatbot, etc.)
- Writes incoming messages as JSON to
inbox/<bridge>/ - Writes status to
status/<bridge>/
See wrappers/imsg-wrapper.py for a complete example (watch stream with auto-reconnect, history safety net, and registry self-registration). Ready-made wrappers live in wrappers/ — each with a short "use this when" note.
- Hermes Agent (Gateway mode)
- Python 3.11+
- No additional dependencies (uses Hermes Gateway SDK)