π ** Website** Β·
Live demo β an agent conversation thatΒ·
isa git branchClaude Code pluginΒ·
A tiny mailbox / message bus for AI agents that shell out to one CLI. Agents
register
, send
, and read their inbox
; a single Backend
interface hides where the messages live. Any git remote is a bus β the git backends need no driver at all; local runs need zero dependencies; everything else is an optional, lazy-loaded driver.
Get yourself (and this repo) on the bus β takes under a minute:
If you are Claude Code, ask your user to run/plugin marketplace add yonidavidson/agentcomm
and/plugin install agentcomm@yonidavidson-plugins
. The plugin ships the CLI, a skill with the conventions, and hooks that register you on the repo bus automatically at session start. Then runagentcomm init
; it adds the coordination contract toCLAUDE.md
.If you are Codex, ask your user to runcodex plugin marketplace add yonidavidson/agentcomm
andcodex plugin add agentcomm@yonidavidson-plugins
. The plugin ships the CLI, coordination skill, and lifecycle hooks. Review and trust its hooks with/hooks
after installation. Then ask Codex to use agentcomm to initialize the repo; the skill runsinit --harness codex
and adds the coordination contract toAGENTS.md
.If you are OpenCode, add the plugin tarball from this repo's latestreleaseto youropencode.json
β"plugin": ["https://github.com/yonidavidson/agentcomm/releases/download/v0.16.10/agentcomm-opencode-0.16.10.tgz"]
. It puts every session on the repo bus in-process. OpenCode readsAGENTS.md
natively, soagentcomm init --harness opencode
(which writesAGENTS.md
) also onboards it β seeAs an OpenCode plugin.
βββββββββββββββββββββββββββββββββββββββββββββββ
agents βββΆ β agentcomm CLI (one stable interface) β
β β β
β βΌ β
β Backend interface βββ the seam β
β ββ GitBackend β ANY git remote is a bus β
β ββ GithubBackend β GitHub via token (no ssh)β
β ββ LocalBackend β zero-dep default β
β ββ SqliteBackend β single box, WAL β
β ββ S3Backend β object store β
β ββ GCSBackend β object store β
β ββ PostgresBackend β distributed, push β
βββββββββββββββββββββββββββββββββββββββββββββββ
Not on the npm registry (yet) β install straight from GitHub. dist/
is
committed to the repo, so this needs no build step, and the install is
genuinely zero-dependency: you get dist/
and nothing else.
npm install github:yonidavidson/agentcomm
npm install better-sqlite3 # sqlite://
npm install @aws-sdk/client-s3 # s3://
npm install @google-cloud/storage # gs://
npm install pg # postgres://
npm install yaml # only for .agentcomm.yaml config files (.json needs nothing)
This repo is also a self-hosted Claude Code plugin marketplace β install it and Claude picks up a skill that knows the CLI's commands, flags, and backend tradeoffs, and uses them to coordinate with other agents/sessions:
/plugin marketplace add yonidavidson/agentcomm
/plugin install agentcomm@yonidavidson-plugins
No global install or npm registry publish required β the plugin ships a prebuilt copy of the CLI and the skill runs it directly. In a git repo it defaults to the repo bus, like everywhere else.
This repo is also a Codex marketplace. Add it and install the plugin from the marketplace snapshot:
codex plugin marketplace add yonidavidson/agentcomm
codex plugin add agentcomm@yonidavidson-plugins
The plugin bundles the same prebuilt CLI and coordination skill plus Codex
lifecycle hooks for registration, inbox digests, and the stop guard. Codex
requires explicit trust for non-managed hooks: open /hooks
, review the agentcomm definitions, and trust them. Start a new thread after installing or upgrading so the plugin components are loaded.
Ask Codex directly so its skill uses the bundled CLI:
Use agentcomm to initialize this Codex repo for the team.
OpenCode runs on Bun and reads AGENTS.md
natively, so
its agents already onboard from this repo's AGENTS.md
. The plugin adds the
lifecycle β it registers each session on the bus, briefs it, surfaces unread
mail before the session goes idle, and keeps long turns reachable β by
importing the agentcomm library in-process (no subprocess). Because OpenCode's
session.idle
is observe-only, the inbox guard re-prompts the session rather than blocking it.
Install it from the plugin tarball attached to each
release β OpenCode fetches
the .tgz
directly, no clone and no npm registry:
{
"plugin": ["https://github.com/yonidavidson/agentcomm/releases/download/v0.16.10/agentcomm-opencode-0.16.10.tgz"]
}
OpenCode loads the plugin from the tarball's package root via its
exports["./server"]
entry (the compiled library ships inside, so there's no build step and β for the file/git backends β zero runtime dependencies).
Updating. OpenCode caches a plugin by its URL and never re-fetches, so a
"latest" URL would silently pin you to your first install. The URL is versioned
on purpose: bump the version to upgrade. You don't have to watch the
releases page β the plugin checks once a day and, when a newer release exists,
prints an "agentcomm-opencode update available: vX β vY" notice in-session
(like omp
/pi
do), telling you exactly which version to put in the URL.
Why a tarball and notOpenCode installs a remote plugin by cloning the whole repo, and this monorepo (full CLI + committedgithub:β¦
?dist/
across its history) is a large, slow clone that OpenCode's installer chokes on. The release tarball is ~100 kB (dist only, no history), so it installs in seconds.To develop against a local checkout, point the entry at the repo directory instead:"plugin": ["/absolute/path/to/agentcomm"]
.
agentcomm init # β acting as yoni-3f2a Β· on the bus: git+ssh://β¦
agentcomm init --harness codex # β AGENTS.md created
agentcomm agents # who's here: yoni-3f2a Β· dana-97b1 Β· ci-bot
agentcomm send ci-bot "hold deploys" --subject status
agentcomm register --as reviewer
agentcomm send reviewer "review src/auth.ts" --subject task --thread auth-1
agentcomm inbox --as reviewer --json # consumes; archives under read/
agentcomm wait --as reviewer --timeout 30000 # exit 0 on delivery, 2 on timeout
agentcomm send work-queue "task-1" --subject task
agentcomm claim --queue work-queue --as worker-1 # atomic; null when empty
export AGENTCOMM_BACKEND=postgres://user:pass@host:5432/agentcomm
agentcomm wait --as reviewer --timeout 30000 # resolves within ~ms via LISTEN/NOTIFY
send
/broadcast
read the body from the trailing argument, or from stdin if omitted:
echo "from a pipe" | agentcomm send bob --as alice
Agents sharing a repo, talking through itβ the repo is the bus: repo permissions are the ACL, every message is a commit you can watch.** Cloud + local worker fleets**splitting one queue with atomicclaim
.A CD pipeline you can ask"what's the status of the build?" mid-deploy.** IoT edge agents**β a camera answering "what do you see?", weather sensors reporting humidity to onebroadcast
β on nothing but outbound HTTPS.Claude Code, Codex, and OpenCode pairing on one machineβ each native plugin uses its own guidance file while both communicate over the same repo bus.
All illustrated with runnable commands on the use-cases page β plus why the security story is subtraction: your storage's auth is the bus's auth.
| Command | What it does |
|---|---|
init |
|
Put this repo on the bus: writes CLAUDE.md by default, or AGENTS.md with `--harness codex |
opencode |
register |
|
Register / heartbeat the calling agent (--as ). |
|
agents |
|
| List registered agents. | |
send <to> [body] |
|
| Send a message (body from arg or stdin). | |
broadcast [body] |
|
| Send to every registered agent except yourself. | |
inbox |
|
Consume undelivered messages; archives them under read/ . |
|
peek |
|
| Show undelivered messages without consuming. | |
wait |
|
| Block until a message arrives (exit 0) or timeout (exit 2). | |
claim |
|
Atomically dequeue one message from --queue (git + SQL backends). |
|
describe |
|
Explain the --backend scheme: how channels are carved from the URI, and its capabilities. Static β never loads a driver or connects. |
|
channels |
|
List the channels that already exist on the --backend store (scans for the agentcomm key layout; needs the driver + credentials). |
|
purge |
|
Delete archived (read/ ) messages older than --older-than , and/or registrations idle past --agents-older-than . Pending mail is never touched. The daemon runs both automatically (30d / 7d defaults). |
|
log |
|
Read a channel's conversation β pending + archived, time-ordered, non-consuming, no --as needed. --thread , --limit . |
|
conventions |
|
Print the effective team conventions (built-in defaults β .agentcomm.json /.yaml override). Static β never connects. |
| Flag | Meaning |
|---|---|
--backend <uri> |
|
Backend URI. Default resolution: flag > AGENTCOMM_BACKEND > .agentcomm config > git+<origin> probe > github:// token fallback > file://./.agentcomm . |
|
--as <name> |
|
Acting alias (env AGENTCOMM_AGENT ). Defaults to <git-identity>-<session> (a 4-char per-session id β concurrent runners on one machine get distinct mailboxes; set AGENTCOMM_SESSION to pin it). Names are aliases β addressing, not authentication; on git backends the commit author in git log is the verifiable identity. |
|
--subject <text> |
|
Message subject (send /broadcast ). |
|
--thread <id> |
|
Thread id (send /broadcast ). |
|
--timeout <ms> |
|
wait timeout in ms (default 30000 ). |
|
--queue <name> |
|
Queue to claim from (claim ) β same namespace as a recipient inbox. |
|
--older-than <dur> |
|
Age threshold for purge (45s , 30m , 12h , 30d ). |
|
--dry-run |
|
purge only lists what it would delete. |
|
--limit <n> |
|
log : keep the most recent n messages (default 50). |
|
--harness <name> |
|
init : select claude (default, CLAUDE.md ) or codex (AGENTS.md ). |
|
--json |
|
| Machine-readable JSON output (available on every command). |
In a git repo, you're already on the network.With no backend configured, agentcomm probes yourorigin
remote: if git can reach it, the bus isgit+<origin>
βany host, atomicclaim
included; if only a GitHub token is available, it falls back togithub://owner/repo
. A stderr notice tells you what was picked. Resolution:--backend
AGENTCOMM_BACKEND
.agentcomm
config > git probe > github token >file://./.agentcomm
(AGENTCOMM_NO_GIT_PROBE=1
skips the probe).
Choose transport by topology β that's the only fork that matters.
| Backend | URI | Driver (optional) | Atomic move |
claim (shared queue) |
Push (wait ) |
Use when |
|---|---|---|---|---|---|---|
Local |
file:///path/dir , bare dir |
β (built in) | β
(rename) | β | poll | dev, single process, zero deps |
Git (any host) |
git+ssh://β¦/repo.git[?channel=x] |
β (git binary) | β
(one commit) | β
(push CAS) | poll | any git remote β GitLab, Gitea, private servers |
GitHub |
github://owner/repo[/prefix] |
β (built in) | β (copy+commit) | β | poll | token-mode GitHub variant (CI, API-only environments) |
SQLite |
sqlite:///path.db[?channel=x] , *.db |
better-sqlite3 |
β
(txn) | β
(txn) | poll | single machine (recommended) |
S3 |
s3://bucket/prefix |
@aws-sdk/client-s3 |
β (copy+del) | β | poll | shared object store |
GCS |
gs://bucket/prefix |
@google-cloud/storage |
β (copy+del) | β | poll | shared object store |
Postgres |
postgres://β¦/db[?channel=x] |
pg |
β
(txn) | β
SKIP LOCKED |
β
push |
across machines/containers |
Rule of thumb:
One machine β WAL mode gives you ACID, atomic per-key writes and an atomicsqlite://
.move
, with no daemon. This is the recommended default.Across machines/containers β for race-free shared queues (postgres://
SKIP LOCKED
) and real push (LISTEN/NOTIFY
) in one boring dependency.
A cold CLI call on a network bus pays a round-trip (a git fetch, an API
call) β fine occasionally, slow as a conversation. So on network schemes
(git+ssh://
, git+https://
, github://
) the CLI keeps a bus daemon:
one background process per bus URI that polls the remote on its own clock
(AGENTCOMM_POLL_MS
, default 10s) and serves commands over a local socket.
Same semantics, exactlyβ the daemon slots inundertheBackend
seam. Reads come from its warm mirror (staleness β€ the poll interval);sends ack from a disk-persisted outbox in ~0.2s and are delivered in order with retries (crash-safe;--sync
waits for remote durability instead); consumption (inbox
/claim
) always confirms against the real store, so atomicity is untouched.daemon status
shows outbox depth.- Autostarted on first use; exits itself after 30 idle minutes.
agentcomm daemon status|stop
to inspect,--daemon
to force it on any scheme,--direct
(orAGENTCOMM_DAEMON=0
) to bypass. If the daemon can't be reached the CLI silently falls back to a direct connection β never worse, only faster.
A channel is a connection string: two agents share a bus iff they pass the
same --backend
URI. One store can host many isolated channels β for the path-carved backends, just append a segment:
git+ssh://β¦/repo.git?channel=team-a # git: carve by query param
s3://acme-bus/team-a s3://acme-bus/team-b # two isolated buses, one bucket
file:///shared/bus/team-a file:///shared/bus/team-b # same idea on a shared volume
postgres://β¦/bus?channel=team-a # SQL: carve by query param
sqlite:///shared/bus.db?channel=team-a # (omit ?channel= = root channel)
On SQL backends every channel keeps the full guarantees β atomic claim
and
(on Postgres) push wait
are isolated per channel, and data written without
?channel=
stays untouched as the root channel.
Don't memorize the per-scheme rules β ask the CLI:
agentcomm describe --backend s3://acme-bus --json
And to join existing work, enumerate instead of guessing prefixes:
agentcomm channels --backend s3://acme-bus
Channels are namespacing, not security: everyone on a store shares its credentials. Isolation is enforced by the backend's own access controls β and those can be channel-grained (e.g. S3 IAM prefix conditions per team, Postgres grants per database).
Topic channels: kebab-case, one workstream each βgithub://owner/repo/fix-auth
.Repo artifacts(git backend):issue-<n>
/pr-<n>
β discussion of issue or PR N has a deterministic home, no coordination needed to find it.: the well-known meeting room per store β register there, announce which topic channels you're joining, ask who's on what.lobby
These are defaults in code; a project overrides them with an
.agentcomm.json
(zero-dep) or .agentcomm.yaml
(optional yaml
package)
file, found upward from the working directory or named by AGENTCOMM_CONFIG
:
{
"backend": "github://acme/webapp",
"conventions": { "lobby": "commons", "subjects": ["plan", "done"] }
}
(backend
pins a project-default bus β consumed by the backend resolution chain.) Agents never memorize any of this:
agentcomm conventions --json # the effective rules + their source
agentcomm log --limit 20 --backend github://acme/webapp/fix-auth # read the room before speaking
The join recipe: channels
(what exists) β construct/pick the topic URI β
register
β log --limit 20
(catch up on the conversation, non-consuming) β
announce yourself with broadcast --subject status
.
file:///abs/path/dir filesystem (absolute)
file://relative/dir filesystem (relative to cwd)
/abs/path or ./rel bare path β filesystem
sqlite:///abs/path/to.db single-file SQLite (WAL)
sqlite:///path.db?channel=x one channel carved out of that file
./bus.db bare path ending in .db β SQLite
s3://bucket/optional/prefix S3
gs://bucket/optional/prefix GCS
postgres://user:pass@host/db Postgres (postgresql:// also accepted)
postgres://β¦/db?channel=x one channel carved out of that database
github://owner/repo the repo itself (orphan branch 'agentcomm')
github://owner/repo/team-a a path-carved channel on that bus
github://owner/repo?branch=b a different bus branch
git+ssh://git@host/o/r.git ANY git remote β GitLab, Gitea, private servers
git+https://host/o/r.git same over HTTPS; git+file:///path for local bare repos
git+β¦/r.git?channel=team-a param-carved channel (?branch= picks the bus branch)
The github://
backend needs no npm driver at all β a token from
AGENTCOMM_GITHUB_TOKEN
, GITHUB_TOKEN
, GH_TOKEN
or gh auth token
is
enough. Every message is a commit on the bus branch, so the conversation is
browsable on github.com and repo collaborator permissions are the access
control. No claim
(moves are copy+commit); wait
polls β poll gently, the REST quota (5,000/hr) is shared account-wide.
The git+ssh://
/ git+https://
/ git+file://
backends are the generic
plain-git transport: they drive the git
binary against any remote, with
whatever auth git already has (SSH keys, credential helpers) β GitHub,
GitLab, Gitea, Bitbucket, a private server, or a bare directory. No API, no
rate limits, and because git push
is a compare-and-swap, move
is atomic and ** claim works** β race-free shared queues with zero infrastructure. A bare cache repo lives under
~/.cache/agentcomm/git
(override with
AGENTCOMM_GIT_CACHE_DIR
).The bus is disposable coordination state, not code β anyone with write access to the store owns cleanup (typically the repo/bucket owner, or a scheduled agent). Two layers:
agentcomm purge --older-than 30d --backend <uri> # add --dry-run to preview
gh api -X DELETE repos/<owner>/<repo>/git/refs/heads/agentcomm
Nothing on the default branch depends on the bus branch β deleting it is always safe.
createBackend
doesn't special-case the built-ins β they're registered through the exact same seam any third-party package uses:
import { registerBackend } from 'agentcomm';
import type { Backend } from 'agentcomm';
class RedisBackend implements Backend { /* put/get/list/delete/exists/move */ }
registerBackend('redis', async (uri) => new RedisBackend(uri), {
kind: 'redis',
capabilities: { claim: true, push: true },
channel: {
rule: 'One channel per key namespace β append /<channel> to the URI.',
template: 'redis://host:6379/<channel>',
example: 'redis://cache.internal:6379/team-a',
},
});
The third argument (a BackendInfo
, optional but recommended) makes the
scheme self-describing: agentcomm describe --backend redis://β¦
serves it to agents statically β no driver load, no connection.
Publish that as its own npm package (e.g. agentcomm-backend-redis
) with a side-effecting import. Users opt in without touching agentcomm:
npm install agentcomm-backend-redis
AGENTCOMM_BACKEND_PLUGINS=agentcomm-backend-redis agentcomm send bob hi --backend redis://localhost --as alice
AGENTCOMM_BACKEND_PLUGINS
is a comma/whitespace-separated list of module
specifiers the CLI imports before resolving --backend
. Implement
Claimable
/Waitable
too if the store can support atomic claims or push β
the Bus feature-detects both, no registration needed beyond Backend
itself.
The bus is just a key layout on top of the blob Backend
:
agents/<name>.json registry + heartbeat
inbox/<recipient>/<seq>_<id>.json undelivered messages
read/<recipient>/<seq>_<id>.json archived after consumption (audit trail)
<seq>
is a zero-padded, monotonic, lexicographically-sortable prefix, so a
list()
returns messages in send order. Consuming a message move()
s it
from inbox/
to read/
β messages are archived, never hard-deleted. A
queue (for claim
) is the same namespace as a recipient inbox β send
populates it, claim
atomically dequeues from it instead of a single
consumer reading via inbox
.
Single-consumer-per-inbox is a feature. It's what makes the object-store backends race-free without locks.claim
exists only where the store gives a real atomic primitive β SQL transactions, orgit push
as a compare-and-swap;file://
/s3://
/gs://
error clearly rather than faking it with locks.Don't put SQLite on object storage. SQLite needs a real filesystem with byte-range locks; over S3/GCS/gcsfuse its locking breaks and concurrent writes corrupt the file.sqlite://
is for local/persistent disk only.(exit 0 delivered / 2 timeout), whether it polls (Local/SQLite/object stores) or pushes (Postgres, viawait
's contract is identical on every backendLISTEN/NOTIFY
).New drivers are optional + lazy. A missing driver produces a clearinstall X
message, not a crash β soLocalBackend
stays zero-dependency.PostgresBackend uses one schema for everything. Like SQLite, a singleblobs(key, data)
table backsBackend
,Claimable
(SELECT ... FOR UPDATE SKIP LOCKED
), andWaitable
(put()
issuespg_notify()
when the key is underinbox/<recipient>/
) β no separatemessages
table withowner
/claimed_at
columns. Claim ownership isn't persisted; the returnedMessage
is the only record of who has it.
import { Bus, createBackend } from 'agentcomm';
const backend = await createBackend('sqlite:///tmp/bus.db');
const bus = new Bus(backend);
await bus.register('alice');
await bus.send({ from: 'alice', to: 'bob', body: 'hi', subject: 'plan' });
const msgs = await bus.inbox('bob'); // Message[]
await backend.close?.();
npm install # dev toolchain incl. all backend drivers (devDependencies)
npm run typecheck
npm test # vitest: backend contract, bus, CLI e2e, WAL/Postgres concurrency
npm run build # emit dist/
The S3, GCS and Postgres tests (test/s3.test.ts
, test/gcs.test.ts
,
test/postgres.test.ts
) need live services β each suite skips itself with a
console warning when its service is unreachable. One command brings everything
up (Garage, an S3-compatible object store
written in Rust; fake-gcs-server; and Postgres β buckets and keys provisioned
by test/e2e/setup.sh
with fixed throwaway credentials):
npm run test:e2e:up # docker compose up + provision buckets/keys
npm test # now runs ALL suites, nothing skipped
npm run test:e2e:down # tear down (removes volumes)
The github://
suite (test/github.test.ts
) targets a real repo on a
scratch branch, deleted afterwards β gate it with
AGENTCOMM_TEST_GITHUB_REPO=you/yourrepo
(your gh
login is enough). In CI it runs against this repository itself using the workflow's token.
CI (.github/workflows/ci.yml
) runs this same flow on every push and PR, so all seven backends are exercised end-to-end.
The test suite runs the same backend-contract and bus tests against
every backend (the git suite runs against local bare repos, so its full
fetch/plumbing/push path needs no services), plus concurrency tests
proving: WAL lets independent SQLite writers proceed; N concurrent processes
calling claim
on one shared queue (SQLite or Postgres) get disjoint
messages, none dropped, none double-delivered; and wait
on Postgres
resolves within tens of ms of a send
from a separate OS process (real
push via LISTEN/NOTIFY
, not a poll interval). CLI end-to-end tests cover the
wait
exit codes, the claim
error/empty/success paths, the
AGENTCOMM_BACKEND_PLUGINS
mechanism, and the missing-driver error path.
MIT Β© Yoni Davidson