README
¶
beb

Async messaging for AI agents. They never have to be running at the same time, or on the same machine, or under the same harness: mail waits, and an agent that starts tomorrow picks up what was said yesterday. Delivery is immediate when both sides are up. No daemon, no SDK, one static binary; the relay is any box you can ssh to.
Why not just type into the other agent's terminal?
If both agents live in your multiplexer and both are running right now, do that — it is simpler and beb adds nothing. That model needs one live process to hold every participant at once, which is exactly what breaks in ordinary use: agents restart on context limits, work runs headless on a box you have no pane into, you close the laptop, or the recipient does not exist yet. Keystrokes into a pane also leave no record, so nothing can answer "was it ever told?"
A queue also decides who controls attention. Typing lands in whatever the agent is doing at that moment; mail waits at the door until the agent chooses to read it, so a long chain of reasoning is not derailed by someone else's status update. When several agents write at once the queue serializes them by seq instead of interleaving keystrokes into one prompt, and a busy agent accumulates mail in order rather than dropping it.
beb assumes the opposite of a shared multiplexer: nothing needs to be alive except sshd, which already is.
Install
One line, no sudo, lands in ~/.local/bin:
curl -fsSL https://getbeb.dev/install.sh | sh
Other routes to the same binary:
go install getbeb.dev/beb@latest # if you have a Go toolchain
nix profile install github:getbeb/beb # via the flake
go build -o beb . # from source; CGO_ENABLED=0
The installer detects OS and arch (linux/darwin, amd64/arm64) and verifies
checksums — mandatory on the release host, advisory on a mirror you point
it at with BEB_URL. It never escalates. BEB_INSTALL_DIR overrides the
destination.
Join a bus that already exists
Someone gave you a relay address. One command:
beb join user@relay:team # your ssh key decides who you are
The join also writes a .beb file: the directory now is that persona
(like a git repo carrying its user.name), so every beb command here just
works with no environment. .beb files layer upward, so a parent
directory can declare the workspace and relay once while each agent
directory beneath it declares only its node.
A box that is one agent needs no persona directory at all: run
beb join from $HOME and it writes ~/.beb/config, which answers
everywhere on that machine — so ssh box then claude just works,
with no cd first. (It cannot be ~/.beb itself: that is the data
directory.) A directory's own .beb still wins, so a laptop running
several personas keeps them apart.
Treat .beb like .env: machine-local, and gitignored. It names an
identity and often a key path, so a shared one would have two people
sending as one node and draining each other's mail. To point a team at
the same bus, share the two lines that are not personal — workspace
and relay — and let each person's beb join fill in the rest.
Then:
beb send bob "the deploy is green" # ok 42 = stored as seq 42
beb send all "standup in 5"
beb recv # print unread, advance your cursor
beb recv 42 # re-read from seq 42 (nothing is lost)
beb recv ~20 # or just the last 20, seqs unread
beb watch # stream messages as they land
beb watch -t 120 # same, but exit 0 after 120s
beb watch -notify # say mail exists, leave it unread
beb ls # roster and unread counts
beb whoami # who this key is, per the relay
beb leave # off the roster
Messages are one line, at most 512 bytes, plain text:
42 2026-08-08T10:57:09Z alice> the deploy is green
That is what is stored, and it never changes. What you see is a
preference: BEB_TIME=hm renders 42 10:57 alice> …, BEB_TIME=none
renders 42 alice> …, and a time line in .beb sets it per
directory. The timestamp is the widest field and the least load-bearing
— order and resume come from seq, and nothing is ever gated on time —
so shortening it costs nothing while the log stays greppable by date.
recv never re-reads history. If your connection drops, reconnect and
resume; nothing is lost, because mail waits and your cursor is yours.
Multi-line content just works — newlines and tabs fold onto the one line
and unfold on the way out. Reply into a thread by starting the body with
re:<seq>:
beb send bob "re:42 agreed, shipping it"
Topics: named channels
A topic (#name) is an inbox many nodes read, each at its own cursor.
Watching or receiving one creates it and starts you at now, so a late
subscriber gets the conversation from here rather than a wall of
history it never asked for:
beb watch #migration # creates it; streams what lands next
beb send #migration "schema first"
beb recv #migration 0 # history on demand, from seq 0
You do not have to watch a topic to be reachable in it. An @name
mention drops a tagged copy into that member's own inbox:
beb send #migration "@bob does the index block writes?"
bob sees [#migration@51] @bob does the index block writes? even if he
never subscribed — the tag says where it lives, so he can answer or pull
the surrounding context with beb recv #migration 45. @all reaches no
one: name the agents you actually need.
Local only (two agents, one machine)
The same verbs with no relay and no network:
beb join team alice # each agent joins under its own name,
# in its own directory (writes .beb)
Inboxes live in ~/.beb/team/. Your inbox is a text file; cat and
grep work on it.
Local mode is the same protocol with the mediator removed, so identity
here is declared rather than proven: every agent runs as one Unix user
and could append to any inbox with echo, which is why beb does not
pretend to check. That is usually the right trade for your own agents on
your own machine, where the failure that actually happens is a
misconfigured node name, not forgery.
If you want the enforced version on one machine, put the mediator back:
run a relay on localhost. Nothing about beb changes -- same binary, same
verbs, the address becomes bus@localhost:team -- because sshd does not
care that the network is 127.0.0.1. It needs sshd running and, like
any relay, its own Unix account: sharing yours would collide, since the
relay's workspace and your client's pointer would both want
~/.beb/team. Worth it when several people share a box, or when the
agents are not all yours; overkill otherwise.
Stand up a relay
A relay is a Unix account on any machine with sshd. On that machine, install beb the same way you would anywhere:
useradd -m bus # a dedicated user; never root
sudo -u bus sh -c 'curl -fsSL https://getbeb.dev/install.sh | sh'
Then, as that user, once per member:
beb grant alice --join team < alice.pub
That is the whole of onboarding: the key says who alice is on this
relay, the roster says which workspaces she may use, and grant --join
does both. It prints the three lines she drops in a .beb file, so her
side is a copy-paste rather than a command. The two halves stay
separable (beb join <ws> <node>) for a key that belongs to several
workspaces with different memberships.
grant writes the authorized_keys line (ForceCommand, restrict, the
lot) so nobody composes one by hand; revoke <node> removes it and touches
nothing else in the file. That is the whole relay: sshd is the daemon, and
there is no other server process. Members now join from their machines with
beb join bus@relay:team.
Ports, jump hosts, and key selection all come from your ssh config — beb
runs your real ssh (BEB_SSH overrides which one). For an air-gapped
relay with no outbound network, cross-compile and copy instead:
GOOS=linux GOARCH=amd64 go build -o beb . && scp beb bus@relay:.local/bin/
Verbs
| verb | what it does |
|---|---|
join <ws> <node> / join user@host:ws |
join a workspace, local or over ssh |
send <to|all> <msg> |
append one line; prints the assigned seq |
recv [<seq>|~<n>] |
print unread and advance your cursor; a seq re-reads from there, ~20 re-reads the last 20 |
watch [-t <secs>] [-notify] [-takeover] |
stream mail; -t bounds the wait, -notify reports without reading, -takeover reclaims a forgotten cursor |
ls |
roster, unread counts, and topics |
whoami |
the node this key is, per the relay |
leave [<node>] |
off the roster, or unsubscribe from a #topic |
grant [--join <ws>] / revoke |
relay-side: bind or remove an ssh key (admin) |
Rules the tools live by
- Identity: locally it is declared -- flags, env, or the directory's
.beb(addressing, like git'suser.name); remotely, your ssh key (authentication). The wire carries no identity claim, so impersonation is unrepresentable rather than checked for.$HOMEnever carries an identity: it is shared ground. - Seq is workspace-global and monotonic, never dense: gaps in your inbox are normal. Order and resume by seq only; timestamps are for reading.
- Ack (
ok 42) means stored, not read. Reading only moves a cursor, so mail another reader consumed is behind you, not gone:beb recv <seq>reads it again. Inboxes are append-only for exactly this reason. - One reader per cursor: a second
watchon the same inbox is refused, because two readers would split the mail between them and neither could tell. First come, first served -- until the first comer is a process nobody listens to any more, sowatch -takeoverasks the incumbent to hand the cursor back and it leaves. That is for delivery infrastructure a person just configured, not for agents: an agent that takes a cursor because it wanted one is how mail ends up split again. re:<seq>at the start of a body is a thread reference, and@namein a topic post reaches that member's own inbox. Both are conventions the relay never parses: they cost no machinery and no verbs.
Scope
A workspace is one trust domain. beb authenticates who sent each message
(the ssh key) but does not encrypt bodies end-to-end and does not defend
members of a workspace against one another — anyone you grant can read
and send within it. Isolate groups that shouldn't see each other by giving
them separate workspaces, not by sharing one. beb is self-hosted on a relay
you control; there is no beb-operated server in the path.
Agent integration
beb is a CLI, so any agent that can run a shell command uses it directly — no server, no SDK. To make an agent reach for beb on its own, give it the context two ways:
-
A skill (
skills/beb/SKILL.md) — the same Agent Skills format Claude Code and Codex both read. One line installs it into whichever agents are on the machine:curl -fsSL https://getbeb.dev/skill.sh | sh # every agent found curl -fsSL https://getbeb.dev/skill.sh | sh -s claude # or name one curl -fsSL https://getbeb.dev/skill.sh | sh -s -- --list # or look firstIt writes one file per agent you name, never creates a config directory for an agent you do not have, and prints the paths, so removing it is
rm -r. For one agent at a prompt, skip the script and say it instead: save https://getbeb.dev/SKILL.md verbatim to~/.claude/skills/beb/SKILL.md(~/.codex/…for Codex). Same file, no pipe to a shell — the script is for provisioning and refreshing across machines, the sentence is for a person onboarding one agent.Skills are per-machine, so every later session there has it: an agent told to "coordinate with the other agent over beb" finds the commands itself.
-
A Claude Code plugin — this repo is also a plugin marketplace, so the native route works too:
/plugin marketplace add getbeb/beb /plugin install beb@getbebThe plugin carries the same skill; pick whichever channel fits (the plugin updates with the repo,
skill.shcovers Codex as well). -
Nothing at all. beb explains itself in band:
joinprints the next step, an injected message carries the flags to reply with, a nudge names the command to run, and a straywatchis refused with the pid that holds the cursor. So the skill loads when it becomes relevant, a project'sAGENTS.mdstays about the project, and there is no second copy of beb's instructions to drift. -
A herdr plugin (getbeb/herdr-beb) — for agents that live in herdr-managed terminals, a bridge injects bus mail straight into the agent's pane as prompts. True push for harnesses with no hook system at all (Codex included): herdr supplies the wake, beb supplies the cross-machine wire and the mailbox.
More
- Website and installer: getbeb.dev
- Releases: github.com/getbeb/beb/releases
- Agent onboarding guide: skills/beb/SKILL.md
- Security policy: SECURITY.md · security@getbeb.dev
- How the site and releases are wired: docs/infra.md
- License: MIT
Documentation
¶
Overview ¶
Command beb is a message bus for agents on different machines.
This binary is all three roles: the local client (this file), the relay's serve mode (serve.go), and the remote client that reaches serve mode over the user's own ssh (remote.go). A workspace with a relay pointer routes its verbs over the wire; one without is the original local bus. Same verbs, same file format, either way.
Directories
¶
| Path | Synopsis |
|---|---|
|
internal
|
|
|
store
Package store is a workspace on local disk: a roster, one append-only inbox per node, a global seq counter, and one cursor per node.
|
Package store is a workspace on local disk: a roster, one append-only inbox per node, a global seq counter, and one cursor per node. |
|
wire
Package wire is the message line format, on disk and on the network.
|
Package wire is the message line format, on disk and on the network. |