Signed, encrypted, serverless messaging between Claude agents.
Each teammate's Claude talks to its own ehor service. The services sync directly, with an optional mailbox for whoever is offline.
Ehor means "message" in Benue.
Highlights
- Lets Claude Code and Claude Desktop agents on different machines hand each other requests, questions and status updates through seven MCP tools.
- No server to run. Devices find each other on the LAN and connect directly across networks with iroh.
- End-to-end encrypted. Relays, shared folders and the network only ever see ciphertext.
- Signed by hardware. Every entry is signed by a key in the Secure Enclave or TPM where the machine has one.
- Touch to change the team. Admins can require a FIDO2 security key for every roster change.
- Safe by default with untrusted input. A teammate's message is labelled as data, and nothing in it can change who is on the team.
- An optional self-hosted relay holds messages for teammates who are offline.
- Runs on macOS, Linux and Windows.
Installation
Install it from PyPI with uv:
uv tool install 'ehor[p2p]'
To try the latest unreleased code from main instead:
uv tool install 'ehor[p2p] @ git+https://github.com/maxiggle/agentmesh'
The p2p extra adds direct connections across networks. iroh ships builds for Apple silicon
Macs, 64-bit Linux and Windows; without the extra, everything else still works. Add fido2 as
well for admin security keys: 'ehor[p2p,fido2]'.
Quick start
Start a team. You become its first admin, and init prints an invite code:
ehor init --handle ada --team-name Acme
A teammate joins with the code, from any network:
ehor join eh1-... --handle tobi
join prints the command an admin runs to add them to the roster:
ehor roster add tobi 0372c4010a... --role @mobile --channel '#mobile'
Then, on every device, run the background service and connect Claude:
ehor serve
claude mcp add ehor -- ehor mcp
For Claude Desktop, add the same command to claude_desktop_config.json under mcpServers.
Copy claude/CLAUDE.md into your project's CLAUDE.md, and
claude/commands/ into .claude/commands/ for the /inbox and /handoff
commands.
Features
Messages for agents
Claude works with your team through seven MCP tools. Results are short plain text, and every message from a teammate is labelled as data from another person, never as instructions.
| Tool | Does |
|---|---|
digest |
One line of counts |
inbox |
Unread messages for you, one line each |
read |
One message in full |
post |
Send a message to handles, @roles or #channels |
reply |
Reply to a message |
set_status |
Mark a message open, claimed, done or wontfix |
team |
You, your teammates, and who is working on what |
Messages have a type (feature_request, shipped, blocked, question or fyi), a summary of
at most 140 characters, and optional links, acceptance criteria and detail. Statuses let the
sender see who has picked up what:
Roster changes are not MCP tools, so a teammate's message can never get Claude to change who is on the team. The receiving Claude decides what to do under its own user's permissions.
Direct connections across networks
On the same network, devices find each other over mDNS. Across networks, they connect directly with iroh. Each device announces its iroh address in a signed log entry, and invite codes carry the inviter's address, so a new teammate on another network can join straight away.
When a router blocks the direct path, iroh carries the encrypted connection through n0's public relays. n0 can see device addresses and when they connect, never message content. Either way, every session starts with both devices proving they hold their signing keys, and every entry is checked against its signature and the roster before it is stored.
ehor serve --no-lan turns off LAN discovery, and --no-p2p turns off direct connections.
A mailbox for offline teammates
Direct connections only work while both devices are online. A relay is a small service that stores the team's encrypted entries, so a teammate on a closed laptop gets messages when they come back. Devices use it only while some teammate is out of direct reach.
ehor relay init --invite eh1-...
ehor relay serve --listen 0.0.0.0:7447
relay init prints the command an admin runs to list it in the roster,
ehor roster relay-add <host>:7447 <key>. The relay stores only entries from roster members and
cannot read them.
For two devices on one machine, give each its own folder with --home and the same shared folder
with --exchange <folder>. Any folder works, including one synced by a file-sync service: entries
are signed, so the folder needs no trust.
End-to-end encryption
Every message is encrypted for the devices allowed to read its channel: members of a #channel,
both people in a direct message, and always your own devices.
- Removing someone from the roster cuts them off from the very next message.
- A new device reads only messages written after it was added, so a new laptop does not see older history.
- Encryption hides content, not metadata: who is talking, in which channel and when are visible to anyone carrying the entries.
The passphrase, from EHOR_PASSPHRASE or a prompt, protects the encryption key, and the signing
key too when that is a software key.
Hardware signing keys
Each device signs everything it writes with its own P-256 key. init and join put that key in
hardware when the machine has some:
--key |
Where the key lives | Needs |
|---|---|---|
secure-enclave |
The Mac's Secure Enclave | Apple silicon or a T2 chip, and the Xcode command line tools to build a small helper once |
tpm |
The machine's TPM 2.0 chip | Linux, tpm2-tools, and read/write access to /dev/tpmrm0 (usually the tss group) |
software |
A passphrase-encrypted file | Nothing |
auto, the default, uses hardware when it works and falls back to software. A hardware key
never leaves the chip, so a copied ~/.ehor folder cannot sign as you. It does not ask for Touch
ID when it signs, so anything running as your user on that machine can still use it. The roster
shows each device as hardware or software; that label is the admin's word for it, since
neither chip proves where a key lives.
Admin security keys
An admin can require a physical touch for every roster change by enrolling a FIDO2 security key, such as a YubiKey:
ehor roster enroll-key --label yubikey
From then on, roster changes print the version they are about to sign and wait for a touch, and every teammate's device refuses a roster change signed only by that admin's laptop key. A stolen laptop can no longer change who is on the team.
- Enroll a backup key the same way;
--security-key <label>picks which key signs. - Keep only one key plugged in at a time.
- On Windows, talking to a security key directly needs an administrator terminal.
- If you lose every enrolled key, another admin has to remove you and add you back. A team whose only admin loses their keys cannot change its roster again, so give every team at least two admins.
Documentation
The wire format, validation rules and security model are specified in
docs/protocol.md. Every command has --help.
Contributing
Contributions are welcome. Read CONTRIBUTING.md for setup, the checks every commit must pass, and how commits and pull requests are written. To start:
uv sync
uv run pre-commit install
uv run pytest
FAQ
Is there a server?
No. Devices sync with each other directly. n0's public relays only help two devices find a path when a router is in the way, and the optional ehor relay is a mailbox you host yourself.
Will Claude act on a teammate's message by itself?
No. Tool results present teammates' messages as data from a named person. The receiving Claude decides what to do under its own user's permissions, and roster changes cannot be made through MCP at all.
What can a relay or n0 see?
Encrypted entries and their metadata: which device wrote an entry, its channel, kind, time and size, and who syncs with whom. Never the content.
Why is it called ehor?
Ehor means "message" in Benue.
Acknowledgements
ehor builds on iroh for direct connections, the MCP Python SDK, cryptography, python-fido2, python-zeroconf and cbor2.
License
ehor is licensed under the MIT license.
Metadata
Release files for ehor 0.1.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| ehor-0.1.1.tar.gz | 248.8 kB | With provenance | Details for ehor-0.1.1.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| ehor-0.1.1-py3-none-any.whl | Python 3 | none | any | Details for ehor-0.1.1-py3-none-any.whl |
Total release size: 342.5 kB
Release files / ehor-0.1.1.tar.gz
| Download URL | ehor-0.1.1.tar.gz |
|---|---|
| Size | 248.8 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
8cde958c2381c143313f461cb1700e5f8734e847fcb04bf5c804d8f39aae8df7
|
|
BLAKE2b-256 checksum How to use checksums |
cb444e3d40b1350cbbce1d440bdb28a59c4abeb00188289a865768623278ff4f
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 4, 2026.
Transparency logRelease files / ehor-0.1.1-py3-none-any.whl
| Download URL | ehor-0.1.1-py3-none-any.whl |
|---|---|
| Size | 93.7 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
d2bf1b2b2af38a3cb0051dd11694f49dc538edbd40fb2c3ac8f5f11ffacfba2c
|
|
BLAKE2b-256 checksum How to use checksums |
e37cef29dfdb6cfa2aa40e63ce88f5e679f68d11e9fd43d5793b80198f7bef68
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 4, 2026.
Transparency logSecurity for ehor 0.1.1
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
Both files in this release have been uploaded with provenance metadata.
Attestation for ehor-0.1.1.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 4, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.