I run Claude Code and Codex side by side, often on more than one machine, and they constantly need things from each other: "tell the other agent the API changed", "ask Codex what it found in the logs". For a while I was the message bus, copying text between terminals. agentnet is what I built to get out of that loop. It's an open-source Go tool that lets coding agents message each other directly, on one machine or across several, with a person approving every machine that joins.
The problem, more precisely
Getting two agents to talk sounds trivial until you look at how agents receive input. Before designing anything, I read through reason-network to see how it gets a local Claude session talking to a local Codex session. The key lesson: MCP is pull, not push. An agent calls a tool only when it decides to, so nothing outside can just "send" a message into a running session. Delivery has to use the hooks agents already expose (session start, before a tool runs, end of a turn) or the agent's own input channel where one exists.
The second thing I noticed was that its identity model resembles Tailscale: machine keys, a central service that signs, and endpoints that verify. That works, but I didn't want a coordination server I'd have to run or trust. I wanted agentnet to be headless.
Architecture
Each machine runs one small daemon. Everything local talks to it over a Unix socket in a private directory, using newline-delimited JSON, and it stores messages, sessions and an outbox in SQLite.
- MCP server.
agentnet mcpis a stdio MCP server, registered asagentnet, with three tools:list_team,send_messageandcheck_inbox. - Agent adapters and hooks. There are adapters for Claude Code and Codex, plus a generic one for any MCP client.
agentnet setupregisters the MCP server and installs hooks, so a message is pushed into a running session where the agent supports it and picked up on the next hook or poll where it doesn't. - Identity and teams. Each machine has an ed25519 signing key and an X25519 encryption key. One person founds a network. Membership is a certificate signed by the founder, and revocation is a signed block.
- Envelopes. Messages between machines are signed, and the body is sealed with HPKE (X25519, HKDF-SHA256, ChaCha20-Poly1305). Other machines on the LAN can see who talks to whom, but not what they say. Replayed messages are rejected.
- Transport and discovery. v1 is deliberately boring: machines on the same network find each other over mDNS and talk directly over HTTP. Anyone on Tailscale or ZeroTier can list static peers instead.
The other requirement I set early was that everything be swappable. The agent adapter, store, transport, discovery, sealer and consent prompt are all Go interfaces, and docs/extending.md walks through adding each one. A relay transport or a new agent should be one more implementation, not a rewrite.
Joining, and controlling misuse
The network is mostly for agents, but agents shouldn't be able to add machines on their own. Joining needs people at both ends:
- The new machine asks to join and shows a 12-character pairing code.
- The joiner reads that code to the founder, who checks it matches and admits the machine.
- The founder reads a team code back, and the joiner checks it before accepting.
On macOS the approval is a system dialog; elsewhere it's a terminal prompt.
Once a machine is in, a few controls limit the damage a confused or prompt-injected agent can do:
- Messages are only delivered into folders I've opted in.
- Mail from other people is held until I approve it.
- I can pause inbound mail, revoke a machine, and audit everything (the audit log records body hashes, not contents).
- Held mail is capped per sender and expires.
The security doc says plainly that consent is a speed bump, not a security boundary. Any process running as my own user can still reach the local daemon.
How it got built
I didn't write most of the code by hand. I wrote a detailed plan of 27 tasks across six phases (bootstrap, local messaging, identity and joins, the LAN network, policy and administration, then release) and had AI coding agents implement it one task at a time. Each task got a fresh implementer, then a separate reviewer, then fix rounds until the review was clean. All of v1 went through a single PR, with CI running race-enabled tests on macOS and Linux, plus end-to-end tests that start real daemons and exchange messages. My job was the design calls, the tradeoffs, and saying no.
Interesting problems
- Revocation that didn't stick. Reloading team state from disk could race with a concurrent merge and quietly drop a fresh revocation. The fix made reload merge monotonically, so blocks only ever accumulate.
- Laptops change networks. The daemon worked out its own addresses once, at startup. After a Wi-Fi change it kept advertising the old IP and silently stopped receiving. Now it re-checks on every gossip tick and re-announces over mDNS when its addresses change.
- Unbounded storage. Nothing ever deleted messages, so an unvetted sender could slowly fill the disk with held mail. Now held mail is capped at 100 per sender and expires after 8 days, and read mail expires after 30 days.
- A "read-only" command that wasn't.
agentnet doctoropened the database through the normal path, which changes permissions and schema. It now uses a truly read-only connection. - Installer edge cases. Ending a shell script with
execskips exit traps, so temp files were never cleaned up. Copying over a binary that's running as a service fails on Linux. Andgh attestation verifyneeds gh to be logged in. The installer now cleans up explicitly, installs with an atomic rename, and falls back to checksum-only verification with a warning when gh isn't logged in. - Everyday friction. macOS's long temp directory paths blow past the Unix socket path limit, so tests use short paths. A CI flake turned out to be background presence gossip racing a test.
Where it is now
v0.1.0 is released for macOS and Linux, on both Intel and ARM. Each archive has a build provenance attestation, and the installer verifies its SHA-256 checksum, plus the attestation when gh is available. Setup is one command. On macOS you can paste it to your coding agent, because approval is a system dialog that reaches you either way. On Linux, run it in your own terminal, since the approval prompt is a terminal one:
curl -fsSL https://raw.githubusercontent.com/purnachand-jaddu/agentnet/main/install.sh | sh -s -- up
Shortly after the release, a fresh Claude session sent a test message to the session that had built agentnet, and the reply went back through the network. The older session had started before setup, so it had no MCP tools and replied through the CLI. That's a small reminder that agents only pick up new tools when they start.
What's next
- More transports behind the same interface: bring-your-own URL, a small relay, and possibly libp2p, so machines on different networks can talk without a VPN.
- Certificate expiry and renewal. v1 relies on revocation alone.
- Smaller polish, like including the sender's machine in reply hints.
Takeaways
- Understand how the platform delivers input before designing messaging for it. MCP being pull-only shaped everything.
- Ship the boring transport first, behind an interface.
- Write down what your security controls don't protect against.
- Independent review catches what implementers can't see in their own code, especially races and "this should be read-only" assumptions.