Antenna — Inter-Host OpenClaw Messaging (v1.5.1)
Send messages between OpenClaw instances over reachable HTTPS via the built-in /hooks/agent webhook.
Prerequisites
Each participating host needs:
- OpenClaw gateway running with hooks enabled (
hooks.enabled: true) - A reachable HTTPS endpoint for
/hooks/agent - Antenna agent registered in gateway config (
agentssection) hooks.allowedAgentIdsincludes"antenna"hooks.allowedSessionKeyPrefixesincludes"hook:antenna"- Host-specific Antenna config in:
antenna-config.jsonantenna-peers.json
Normal path:
- Run
antenna setupto generate the live runtime files. - Use
antenna-config.example.jsonandantenna-peers.example.jsonas tracked reference templates only.
Notes:
- Peers do not need to share one tailnet or one central hub.
- Tailscale Funnel is a convenient default, but reverse proxies, VPS/domain-hosted HTTPS, Cloudflare Tunnel, and similar paths also work.
Architecture
Messages flow through a script-first relay pipeline:
- Sender runs
antenna-send.shwhich builds an[ANTENNA_RELAY]envelope and POSTs it to the recipient's/hooks/agentendpoint. - Recipient gateway dispatches to the dedicated Antenna agent.
- Antenna agent writes the raw inbound message to a temp file using the
writetool. - Antenna agent execs
antenna-relay-deliver.shwith that file path as its single argument. - The wrapper reads the file, invokes the relay engine internally, handles verification + local delivery + cleanup, and prints one status line.
- Message appears persistently in the target conversation thread when accepted.
The LLM never performs relay parsing, delivery formatting, or session-routing logic; the scripts do all processing.
Trust Model
Antenna trust is layered:
- Peer URL — where to reach that installation
- Hook bearer token — protects webhook ingress
- Per-peer runtime identity secret — authenticates claimed sender identity when configured. Verified via constant-time comparison; no plaintext secrets land in relay logs.
- Peer allowlists — explicit inbound and outbound peer lists
- Inbound session allowlist — limits where inbound relay may deliver (full session keys only)
- Envelope marker guard — messages whose bodies or header values contain the envelope markers
[ANTENNA_RELAY]/[/ANTENNA_RELAY]are rejected as malformed (prevents envelope smuggling) - Message freshness window — each message carries a
timestamp:; stale or future-dated messages are rejected. Defaults: 300s max age, 60s max future skew. Tunable via.security.max_message_age_secondsand.security.max_future_skew_seconds. - Rate limiting — per-peer and global throttles
- Untrusted-input framing — reminds receiving agents the relayed content may be external
- Log sanitization — peer-supplied values stripped of control characters before logging
- File-permission audit —
antenna statusflags any token/secret file looser thanchmod 600 - Self-id required — sender refuses to run without
self_idconfigured; it does not fall back to$(hostname)
For peer onboarding, Antenna now prefers Layer A encrypted bootstrap exchange using age. Legacy raw-secret export refuses non-TTY output (no piping runtime identity secrets into captured automation).
Configuration
Live runtime files are local installation state:
antenna-config.jsonantenna-peers.json
Tracked reference files live beside them:
antenna-config.example.jsonantenna-peers.example.json
Use antenna setup for normal installation; use the *.example.json files for schema reference or manual recovery.
antenna-config.json
{
"max_message_length": 10000,
"default_target_session": "agent:betty:main",
"relay_agent_id": "antenna",
"relay_agent_model": "openai/gpt-5.4-nano",
"local_agent_id": "<your-agent-id>",
"install_path": "<absolute-path-to-this-skill-directory>",
"log_enabled": true,
"log_path": "antenna.log",
"log_max_size_bytes": 10485760,
"log_verbose": false,
"mcs_enabled": false,
"mcs_model": "sonnet",
"inbox_enabled": false,
"inbox_auto_approve_peers": [],
"inbox_queue_path": "antenna-inbox.json",
"allowed_inbound_sessions": ["agent:betty:main", "agent:betty:antenna"],
"allowed_inbound_peers": ["<peer-a>", "<peer-b>"],
"allowed_outbound_peers": ["<peer-a>", "<peer-b>"],
"rate_limit": {
"per_peer_per_minute": 10,
"global_per_minute": 30
},
"security": {
"max_message_age_seconds": 300,
"max_future_skew_seconds": 60
}
}
Key fields:
relay_agent_model— use a full provider/model ID, not a local aliaslocal_agent_id— used by local CLI conveniences when expanding bare names to full session keys likeagent:<id>:maininstall_path— absolute path to this skill directoryallowed_inbound_sessions— inbound delivery allowlist (full session keys, e.g.agent:betty:main)allowed_inbound_peers/allowed_outbound_peers— peer allowlistsrate_limit.*— inbound abuse controlssecurity.max_message_age_seconds/max_future_skew_seconds— freshness-window tolerance (defaults shown; omit the block to use defaults)
antenna-peers.json
{
"<your-host-id>": {
"url": "https://<your-reachable-hostname>",
"token_file": "secrets/hooks_token_<your-host-id>",
"peer_secret_file": "secrets/antenna-peer-<your-host-id>.secret",
"exchange_public_key": "age1...",
"agentId": "antenna",
"display_name": "My Host",
"self": true
},
"<remote-peer-id>": {
"url": "https://<remote-reachable-hostname>",
"token_file": "secrets/hooks_token_<remote-peer-id>",
"peer_secret_file": "secrets/antenna-peer-<remote-peer-id>.secret",
"exchange_public_key": "age1...",
"agentId": "antenna",
"display_name": "Remote Host"
}
}
Key fields:
url— reachable HTTPS hook base URLtoken_file— bearer token for that peerpeer_secret_file— per-peer runtime identity secretexchange_public_key— peer'sagepublic key for Layer A exchangeself— marks the local host entry
Usage
Send a message
scripts/antenna-send.sh <peer> "Your message here"
antenna msg <peer> "Your message here" # recipient resolves target session
antenna msg <peer> --subject "Config sync" "Here's the block you need..."
antenna msg <peer> --session "agent:<agent-id>:mychannel" "Your message" # explicit session override
echo "Long message body..." | antenna send <peer> --stdin
antenna send <peer> --dry-run "Test message"
Session resolution: When
--sessionis omitted,target_sessionis left out of the envelope entirely. The recipient resolves from their owndefault_target_sessionconfig. You don't need to know another host's internal session layout.
Peer pairing (interactive wizard)
antenna pair # Full interactive wizard
antenna pair --peer-id myserver # Pre-fill peer ID
The wizard walks through keypair generation, public key sharing, optional ClawReef invite, bundle creation, optional bundle email send when mail tooling is available, exchange, connectivity test, and first message — with Next/Skip/Quit at each step. Also auto-offered at the end of antenna setup.
Peer onboarding / bootstrap exchange (manual)
Preferred encrypted flow:
antenna peers exchange keygen
antenna peers exchange pubkey
antenna peers exchange initiate <peer-id> --pubkey <age1...> --print
antenna bundle verify <bundle-file> # read-only: decrypt & sanity-check before importing
antenna bundle verify <bundle-file> --json # machine-readable verdict
antenna bundle verify <bundle-file> --force-expired # inspect a past-expiry bundle without importing
antenna bundle verify <bundle-file> --no-decrypt # treat file as already-decrypted bundle JSON
antenna peers exchange import <bundle-file> # refuses expired bundles
antenna peers exchange import <bundle-file> --force-expired # disaster-recovery override
antenna peers exchange reply <peer-id>
Optional direct-send convenience (email):
antenna peers exchange initiate <peer-id> \
--pubkey <age1...> \
--email someone@example.com \
--send-email [--account <himalaya-account-name>]
Legacy/manual fallback:
antenna peers exchange <peer-id> --export # interactive TTY only; refuses to pipe secrets
antenna peers exchange <peer-id> --import <file>
antenna peers exchange <peer-id> --import-value <hex>
Peer registry updates:
antenna peers add <peer-id> --url <https-url> --token-file <path> # first time only
antenna peers add <peer-id> --url <new-url> --force # update existing: merges only the flags you pass
Notes:
- Secure Layer A requires
ageandage-keygen - Export never materializes plaintext bundle JSON on disk;
jqstreams directly intoage. Import decrypts to a temp file but cleans up on return, validation failure, preview failure, write failure, andCtrl-C(SIGINT/SIGTERM). antenna bundle verify <file>is a read-only sanity check — it decrypts in place, validates shape / endpoint URL / freshness, and prints a safe summary (never the raw hooks token or identity secret). It never writes toantenna-peers.jsonorantenna-config.json. Use it beforepeers exchange importwhen a bundle comes from an untrusted or unclear channel.- Expired bundles are refused by default; use
--force-expiredonly for genuine disaster recovery. - Optional direct-send requires
himalaya. The sender email is resolved from your Himalaya TOML config (${HIMALAYA_CONFIG:-~/.config/himalaya/config.toml},[accounts.<name>] email = "...") — there is noantenna@localhostfallback and no free-textFrom:override. Pass--account <name>to pick a specific configured account; interactive flows use selection-only UX. - Email is convenience transport only, not part of the trust model.
- Import shows a preview and asks before allowlist changes unless
--yesis used. antenna peers addrefuses to overwrite an existing peer without--force;--forcedoes a field-level merge so unspecified peer fields (includingexchange_public_key,self, and any future metadata) are preserved.antenna peers removeprunes peer-scoped allowlist entries (allowed_inbound_peers,allowed_outbound_peers, peer-scoped inbound sessions) so removing a peer does not leave stale allowlist debris behind. Peer secret files are intentionally left in place; secret deletion is an explicit operator action (seeantenna doctorsection 6b for secrets-hygiene warnings about leftover files).
Session allowlist management
antenna sessions list # Show allowed inbound session targets
antenna sessions add antv3 # Bare name → auto-expanded to agent:<local>:antv3
antenna sessions add "agent:marie:lab1" # Cross-agent: use full session key
antenna sessions remove antv3 # Remove (bare names are expanded)
antenna sessions remove "agent:betty:main" --force # Core sessions need --force
Controls which session targets inbound messages can request via allowed_inbound_sessions in antenna-config.json.
Convention: full session keys everywhere. The allowlist stores full keys like agent:betty:main and agent:marie:lab1. The relay requires full keys from senders — bare names are rejected. The CLI auto-expands bare names to agent:<local_agent>:<name> for convenience when adding/removing, but the stored value is always the full key.
Core sessions (agent:<local>:main, agent:<local>:antenna) are protected from removal unless --force is used. Supports batch add/remove.
Health and status
antenna doctor
antenna uninstall --dry-run
antenna uninstall
antenna peers list
antenna peers test <id>
antenna status
antenna log --tail 50
antenna doctor includes warn-only drift audits that complement the hard config/permission checks:
- Section 1b — Peer-State Drift. Audits
allowed_inbound_peers,allowed_outbound_peers, and peer-scoped inbound sessions inantenna-config.jsonagainstantenna-peers.json. Orphan peer IDs (allowlist entries for peers that no longer exist) are warnings, never failures. Catches thenexus/bruce-era debris class automatically. - Section 6b — Secrets Directory Hygiene. File-side counterpart to 1b. Warns on orphan peer-scoped secret / token files in
secrets/(antenna-peer-<id>.secret,hooks_token_<id>,peer_secret_<id>whose<id>is no longer inantenna-peers.json), backup-pattern leftovers (.bak*,.backup*,~,.old), loosesecrets/directory permissions (target700), loose per-file permissions on secret-shaped files (target600), and unknown-shape files insidesecrets/.
Testing
antenna test <model>
antenna test-suite --tier A
antenna test-suite --model <m>
antenna test-suite --models "<m1>,<m2>"
antenna test-suite --report
Model tests emit a per-run TEST_NONCE and match both success and pre-delivery rejections by that nonce, so parallel or historical runs cannot contaminate each other's verdicts and auth / peer / rate-limit failures return promptly instead of waiting for the full timeout. Tests drive gateway config through the CLI/helper path with a single batched restart rather than restarting per operation.
Inbox (optional approval queue)
When inbox_enabled is true in config, inbound messages from peers not in inbox_auto_approve_peers are queued for review instead of being relayed immediately. Auto-approved peers bypass the queue and relay instantly (current behavior).
antenna inbox # list pending messages (table view)
antenna inbox count # pending count (for heartbeat/cron checks)
antenna inbox show <ref> # full message body for a ref
antenna inbox approve all # approve everything pending
antenna inbox approve 1,3,5-7 # selective approval (commas and ranges)
antenna inbox deny all # reject everything pending
antenna inbox deny 2,4 # selective denial
antenna inbox drain # deliver all approved (gateway sessions.send), remove denied
antenna inbox clear # purge all processed items
Delivery flow: antenna inbox drain iterates every approved item and delivers each via openclaw gateway call sessions.send (the same gateway RPC the relay path uses). On success, the item transitions to delivered; on RPC failure it transitions to failed with last_error recorded for triage. Denied items are removed. The script returns non-zero if any delivery failed, prints a one-line summary on stderr, and logs each per-ref result to antenna.log. The calling agent's role is a single exec of antenna inbox drain — no MCP tool calls required.
Configuration:
{
"inbox_enabled": false,
"inbox_auto_approve_peers": ["trusted-peer-id"],
"inbox_queue_path": "antenna-inbox.json"
}
Notes:
- Disabled by default — existing behavior is unchanged
- Auto-approve list lets trusted peers bypass the queue (progressive trust)
- Queue file is local runtime state (gitignored)
- Ref numbers auto-increment and support range selection
- The relay agent now uses only
write+exec; it never callssessions_senddirectly. Drain also stays in script-only territory — it shells out toopenclaw gateway call sessions.send, so cron jobs can drain the queue without an agent in the loop.
Heartbeat / cron integration:
Add to your HEARTBEAT.md:
## Antenna inbox check
- Run: `antenna inbox count`
- If > 0: run `antenna inbox list` and mention it
Or set up a cron job for automated handling:
Check antenna inbox. If there are pending messages from peers
in [trusted-peer-id], approve and drain them. For anything else,
summarize the queue and ask me.
Conversational usage: Ask your assistant "any Antenna messages waiting?" — it can run antenna inbox list, you review, then say "approve 1 and 3, deny 2" and it handles the rest.
ClawReef — Peer Discovery
clawreef.io is the optional community registry for Antenna hosts:
- Discover peers — browse and search the directory
- Send invites — ClawReef delivers them via Antenna to the recipient's default session
- Accept & pair — accepting an invite starts the normal
antenna pairflow locally
ClawReef stores webhook credentials (hooksToken, identitySecret) for push delivery alongside public keys and endpoints — standard webhook-provider behavior. It does not store messages, private age keys, or message content. All trust decisions remain local to Antenna.
The pairing wizard (antenna pair) offers ClawReef invites as an alternative to manual encrypted exchange. Setup also displays ClawReef info after completion.
Security Notes
- Relay agent is script-first and non-interpreting
- Inbound sessions are allowlisted (full session keys only)
- Sender peer must be allowlisted on both inbound and outbound sides
- Per-peer identity secret can authenticate sender claims; comparison is constant-time
- Envelope marker guard rejects messages whose bodies or headers contain
[ANTENNA_RELAY]/[/ANTENNA_RELAY] - Message freshness window rejects stale or future-dated envelopes (defaults: 300s age, 60s future skew)
- Sender refuses to run without configured
self_id(no$(hostname)fallback) - Legacy raw-secret export refuses non-TTY output
- Encrypted bundle export never writes plaintext; encrypted bundle import cleans up plaintext on every exit path (return / fail / SIGINT / SIGTERM)
- Expired encrypted bundles are refused at import (
--force-expiredis the disaster-recovery override) - Email send for bootstrap/pubkey resolves sender address from Himalaya TOML config; no
antenna@localhostfallback, no free-textFrom:override - Tokens and secrets are file-backed and should be
chmod 600;antenna statusaudits permissions - Relay temp files are created with
umask 077, chmod 0600, and shredded before unlink on cleanup - Setup preserves an existing gateway
hooks.tokenrather than overwriting it - Relayed content is framed as potentially untrusted input
- Rate limiting throttles inbound bursts; transaction locking protects inbox and rate-limit state under concurrent access
Troubleshooting
- Gateway won't start: Run
antenna doctor - Want a clean slate: Run
antenna uninstall(use--dry-runfirst if you want a preview) - 401 Unauthorized: wrong hook bearer token
- 403 Forbidden: session prefix/agent restrictions or peer policy mismatch
- Relay rejected: peer not allowlisted, session not allowlisted, or identity secret mismatch
Relay rejected: timestamp out of range (stale|future): peer clock skew exceeds freshness window; sync clocks or widen.security.max_message_age_seconds/.security.max_future_skew_secondsRelay rejected: marker in body|headers: envelope-marker guard working as intended; rephrase or encode any literal[ANTENNA_RELAY]/[/ANTENNA_RELAY]contentself-id not configured - run antenna setup: sender is missing host identity inantenna-config.json; there is no$(hostname)fallback- Encrypted exchange fails immediately:
age/age-keygenmissing Bundle expired - refusing import: request a fresh bundle from the peer, or pass--force-expiredonly for disaster recovery. To inspect an expired bundle without importing, useantenna bundle verify <file> --force-expired.antenna bundle verify: decrypt failed: the bundle was encrypted for a differentagepublic key than yours. Ask the peer to re-initiate against your currentantenna peers exchange pubkey.antenna bundle verify: endpoint URL rejected: the bundle'sfrom_endpoint_urlis not a valid HTTPS URL (e.g.main, bare host). Refuse to import; ask the peer to regenerate after fixing their self-peer URL.antenna doctor: self-peer URL is not a valid URL: your ownselfpeer entry has a malformedurl. Fix it inantenna-peers.jsonor rerunantenna setupwith a valid--url <https://host>. REF-1313 now rejects malformed URLs at input time, but stale pre-fix entries still need to be corrected.antenna doctor: orphan peer references in config allowlists(warning, section 1b): allowlists inantenna-config.jsonreference peer IDs that no longer exist inantenna-peers.json. Remove the stale IDs withantenna peers remove <id>on any current peer (which also prunes its allowlist entries), or editantenna-config.jsondirectly.antenna doctor: orphan secret file/stale backup file/secrets/ dir is not 700(warnings, section 6b): hygiene findings on thesecrets/directory. None of these can authenticate a peer that isn't in the registry, but they are real leak-surface / drift signals. Move orphan files tosecrets.retired/(or delete), rotate or remove.bak*leftovers, and runchmod 700 secrets//chmod 600 secrets/<file>to tighten permissions.Email send fails: could not resolve email for account: addemail = "..."under[accounts.<name>]in your Himalaya TOML config, or pass--account <other>to pick a configured account that has anemailsetEmail send fails: himalaya not installed: installhimalayaor fall back to sending the bundle file by handLegacy export refused - not a TTY:antenna peers exchange <peer> --exportmust run in an interactive terminal; switch toantenna peers exchange initiatefor automated or remote operator handoff- Message sent but not visible: ensure
tools.sessions.visibility = "all"andtools.agentToAgent.enabled = trueon the receiver; the relay delivery wrapper uses gateway session delivery, which still depends on those settings. Also ensuresandbox: { mode: "off" }on the Antenna agent — sandboxed sessions silently clamp visibility totree, blocking cross-agent delivery - Exec denied / allowlist miss: ensure relay agent instructions use only simple commands (no
$(...), heredocs, or chaining); theantenna-relay-deliver.shwrapper accepts a file path only - Repeated approval prompts: ensure Antenna agent has
sandbox: { mode: "off" }in registration. Default advice is not to settools.exec.securityortools.exec.askon the Antenna agent — explicit exec overrides cause silent relay failure (fixed in v1.2.14). If you've intentionally customizedtools.execon the agent, setup reruns now preserve your overrides instead of wiping them. antenna peers addrefuses to update an existing peer: by design — pass--forceto update fields on a paired peer; without it, the command refuses to clobber trust material
File Inventory
skills/antenna/
├── SKILL.md
├── README.md
├── CHANGELOG.md
├── antenna-config.example.json
├── antenna-peers.example.json
├── antenna-peers.json
├── antenna-config.json
├── antenna.log
├── install.sh
├── bin/
│ └── antenna.sh
├── scripts/
│ ├── antenna-send.sh
│ ├── antenna-relay.sh
│ ├── antenna-relay-deliver.sh # v1.4+ — canonical single-call deliver wrapper
│ ├── antenna-relay-file.sh # internal file-based relay adapter
│ ├── antenna-relay-exec.sh # v1.1.6 — base64 wrapper (legacy fallback)
│ ├── antenna-pair.sh # v1.1.9 — interactive peer pairing wizard
│ ├── antenna-health.sh
│ ├── antenna-peers.sh
│ ├── antenna-doctor.sh
│ ├── antenna-exchange.sh
│ ├── antenna-inbox.sh
│ ├── antenna-model-test.sh
│ └── antenna-test-suite.sh
├── references/
│ ├── ANTENNA-RELAY-FSD.md # Relay architecture contract
│ └── issues.md # Known issues / gaps tracker
├── docs/ # Repo-only (operator / historical)
│ ├── full-removal-checklist.md
│ ├── SECURITY-ASSESSMENT-v1.0.20.md
│ ├── RED-TEAM-REPORT-v1.0.4.md
│ ├── LAYER-A-SECRET-EXCHANGE-PLAN.md
│ └── SECRET-EXCHANGE-OPTIONS.md
└── agent/
├── AGENTS.md
└── TOOLS.md
Notes:
antenna-config.json,antenna-peers.json, andantenna-inbox.jsonare local runtime files (gitignored)antenna-config.example.jsonandantenna-peers.example.jsonare tracked reference templates
Gateway / Agent Registration
antenna setup handles all of this automatically and is safe to rerun (e.g., after a clawhub update). Setup forces sandbox.mode = "off" and seeds a default tools.deny list only when absent. It preserves an existing gateway hooks.token and, on rerun, preserves any tools.exec overrides the operator has intentionally set on the Antenna agent.
On each host:
- agent
antennaregistered in OpenClaw config underagentswith:agentDirandworkspaceboth pointing to the Antennaagent/directorysandbox: { mode: "off" }(required — sandbox silently clamps session visibility, breaking cross-agent relay)- restrictive
tools.deny(block web, browser, image, cron, memory tools) - Default advice: do not set
tools.exec.securityortools.exec.askon the Antenna agent — explicit exec overrides cause silent relay failure (see v1.2.14 changelog). If you've intentionally customized these, setup reruns now preserve your overrides rather than wiping them.
hooks.allowedAgentIdsincludes"antenna"hooks.allowedSessionKeyPrefixesincludes"hook:antenna"tools.sessions.visibilityset to"all"(required for cross-session relay delivery)tools.agentToAgent.enabledset totrue
Support
- 📧 Email: help@clawreef.io
- 🐛 Issues: github.com/cshirley001/openclaw-skill-antenna/issues
- 🔒 Security: See SECURITY.md