GitHub - jahala/pollen: peer messaging between AI agents, with a human at the gate — one file, zero dependencies.

GitHub

5 min read Original article ↗

carry what matters.

Peer messaging between AI agents, with a human at the gate. One file, zero dependencies.

What it does

One agent asks another a question and uses the answer, without a supervisor having to relay it. Works with any MCP-compatible agent (Claude Code, Codex CLI, Gemini CLI, etc.).

  • Same machine: messages go through file mailboxes at /tmp/pollen/
  • Different machines: messages route through an HTTP relay
  • The gate: strangers knock; you decide. An agent nobody has vouched for cannot simply talk to yours

Install

Tell your agent:

Install pollen from https://github.com/jahala/pollen

Or do it manually:

curl -O https://raw.githubusercontent.com/jahala/pollen/main/pollen.mjs

Then add to your .mcp.json:

{
  "mcpServers": {
    "pollen": {
      "command": "node",
      "args": ["./pollen.mjs"],
      "env": { "POLLEN_ID": "my-agent" }
    }
  }
}

That's it. The agent now has five tools: pollen_send, pollen_agents, pollen_inbox, pollen_allow, pollen_deny.

Getting woken

An agent that has to remember to check its inbox will miss things. Run the watcher as a background task instead:

node pollen.mjs --watch my-agent

It prints one line per message and keeps running, so any harness that reads lines can rouse the agent — Claude Code's Monitor, a & in a shell, systemd, a tmux pane:

[pollen] message from beta — pollen_inbox to read: the Q4 answers are in /tmp/drop
[pollen] stranger is knocking — pollen_allow or pollen_deny

The line is a doorbell, not the message: pollen_inbox is still what hands the text over. Messages held at the trust gate ring without quoting what they said.

On startup it also rings once for anything already outstanding, counting the two separately — because they need different things from you:

[pollen] 2 messages waiting — pollen_inbox to read
[pollen] 1 knock waiting at the gate — pollen_allow or pollen_deny

A denial is never rung out. Echoing the text a human just rejected onto the channel they watch would defeat the point of denying it.

Don't watch /tmp/pollen/<id>/inbox directly. Those files are consumed within milliseconds of landing, so a poll almost always finds an empty directory. Watch the journal — it is append-only and nothing is ever removed from it.

Cross-machine

Start the relay on one machine:

Expose it (free, no account):

cloudflared tunnel --url http://localhost:4747

Point remote agents at the URL:

"env": {
  "POLLEN_ID": "remote-agent",
  "POLLEN_RELAY": "https://verb-noun-thing.trycloudflare.com"
}

Any tunnel or VPN works. pollen just speaks HTTP to a URL.

The gate

Strangers knock; you decide. An unvouched peer is held, and a person allows or denies it.

The sender is told, rather than left guessing — pollen_send reports what the receiver actually did with the message:

Sent to beta.
Held at beta's trust gate — beta must allow "alpha" before this is delivered.
Queued for beta — no confirmation from beta's pollen; it stays in the mailbox until taken.

An agent waiting on a reply that can never come is worse than one told to go ask a human. Over a relay only the first is possible: the gate runs in the receiver's process on the far side, and its verdict has no route back, so cross-machine senders learn the message left but not whether it was let in.

Knocks queue rather than replace each other, so a stranger's follow-up doesn't destroy the message it's asking about — pollen_allow delivers all of them, oldest first.

Pre-approve agents you trust:

"env": {
  "POLLEN_ID": "my-agent",
  "POLLEN_ALLOW": "trusted-agent-1,trusted-agent-2"
}

Environment variables

Variable Required Description
POLLEN_ID yes Agent name
POLLEN_RELAY no Relay URL for cross-machine
POLLEN_ALLOW no Comma-separated trusted agent IDs
POLLEN_DIR no Mailbox root (default /tmp/pollen)
POLLEN_PORT no Relay port (default 4747)

How it works

~580 lines of JavaScript. Raw MCP protocol over stdio (no SDK). Three modes:

  • node pollen.mjs — MCP server that Claude Code (or any MCP host) spawns as a subprocess
  • node pollen.mjs --relay — HTTP relay with SSE streams for real-time delivery
  • node pollen.mjs --watch ID — tails an agent's journal, one line per new message

Local transport uses the filesystem. Remote transport uses HTTP + SSE. The agent doesn't know or care which one it's using.

Every arrival is appended to /tmp/pollen/<id>/journal.jsonl before the mailbox file is removed, and /tmp/pollen/<id>/cursor records how far the agent has read. That journal is the inbox: a message that arrived but was never read is still there after the MCP server restarts, and on startup the agent is told how many are waiting. Both transports write it, so a relayed message is as durable as a local one.

Notes

  • pollen_inbox lets any agent check for received messages. Claude Code agents also get automatic push via channels (research preview), but pollen_inbox works everywhere.
  • The journal is append-only and never rotated. It lives under /tmp, which the OS clears on reboot.
  • A message held at the trust gate survives a restart — the gate is rebuilt from the journal, so a knock you were rung about is still there to allow or deny.
  • Tests: node --test. They drive real MCP servers over stdio against a real mailbox — nothing is mocked.
  • Requires Node.js 18+. Nothing else.

Support

"Buy Me A Coffee"


Part of the plotplot garden · built with petals