beb

command module
v0.8.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 9, 2026 License: MIT Imports: 16 Imported by: 0

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's user.name); remotely, your ssh key (authentication). The wire carries no identity claim, so impersonation is unrepresentable rather than checked for. $HOME never 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 watch on 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, so watch -takeover asks 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 @name in 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 first
    

    It 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@getbeb
    

    The plugin carries the same skill; pick whichever channel fits (the plugin updates with the repo, skill.sh covers Codex as well).

  • Nothing at all. beb explains itself in band: join prints the next step, an injected message carries the flags to reply with, a nudge names the command to run, and a stray watch is refused with the pid that holds the cursor. So the skill loads when it becomes relevant, a project's AGENTS.md stays 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

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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL