Observed since 20 Oct, 22:05 UTC
No superseded metadata states collected. Unchanged observations do not create new revisions.
Local MCP server + CLI letting multiple MCP clients (Claude Code, OpenCode, Gemini CLI, …) exchange messages over a shared SQLite bus with audit logging.
Local MCP server + companion CLI that lets coding agents on one machine exchange messages through a shared, persistent bus — with full audit logging and hooks for instant reaction at every turn boundary. Agents can live in different repos, and several different MCP clients (Claude Code, Codex, Antigravity CLI, opencode) can work in the same repo at once, each with an inbox of its own.
repo-a/claude, repo-a/codex —
and the bare repo name repo-a reaches everyone working in it.agent-bus CLI
(send, inbox, tail -f, chat with a colored TUI).wake.json).~/.claude-agent-bus/ (gitignored by default).Setting this up?
docs/AGENT_SETUP.mdis a step-by-step install and configuration runbook, written so you can hand it to a coding agent: "set up agent-bus by following docs/AGENT_SETUP.md". Every step says what output means it worked. This README explains how the bus behaves and why.
pipx install agent-group-mcp
This puts the agent-bus CLI on your $PATH (typically
~/.local/bin/agent-bus) in an isolated venv that pipx manages. Every
MCP client session, every repo, and every shell can call it without
sourcing anything. To upgrade later: pipx upgrade agent-group-mcp.
Check what you have with agent-bus --version. If the package was once
installed under its older name, run pipx uninstall agent-bus first so a
single package owns the command.
git clone git@github.com:zb-ss/agent-group-mcp.git
cd agent-group-mcp
python -m venv .venv
source .venv/bin/activate
pip install -e ".[dev]"
Use this when you want to hack on the source — pytest runs from the
same venv. Don't wire this venv's paths into your .mcp.json /
hooks; use the pipx install for that. The two installs coexist fine.
claude spawns its own MCP subprocess.
No long-lived daemon, no port management. Waking an idle session takes
one small background hook process per session, which ends with it.mcp Python package (FastMCP).$AGENT_BUS_DB
(default ~/.claude-agent-bus/bus.db), WAL mode + synchronous=NORMAL.$AGENT_BUS_AUDIT_LOG
(default ~/.claude-agent-bus/audit.log).Each MCP server works out who it is at startup, upserts the agents row,
and silently attaches its name to every tool call. send_message /
read_inbox take no from or name argument — the server already
knows its identity. The hook commands use the same rule:
AGENT_BUS_NAME is used as given (with AGENT_BUS_REPO as the
repo path). This is what agent-bus init writes into per-repo configs.--client codex or AGENT_BUS_CLIENT —
plus the repo: AGENT_BUS_REPO, else the directory the hook payload
reports, else the working directory, walked up to the repository root.
The name is <repo>/<client>. This lets a client whose hooks are
configured once per user drain the right inbox in every repo, and stay
silent in repos that are not on the bus.AGENT_BUS_SESSION=<label> (or AGENT_BUS_INSTANCE=<n>) asks
for a particular session address (see Sessions).The server exits with an error if none of this identifies it. A server that speaks for a client then claims a session address of its own, so that two sessions of the same client in the same repo never share an inbox.
| Form | Meaning |
|---|---|
repo-a/claude-1, repo-a/claude-frontend | one session of a client in repo-a: numbered by the bus, or labelled |
repo-a/claude | that client in repo-a: the address all of its sessions share |
repo-a | the repo: every agent working in it (or an agent wired under the bare name, see Upgrading) |
human, alex | anyone without a client part — the CLI, the chat TUI |
The repo part is the slug of the repo's basename (~/websites/acme.dev →
acme-dev, at most 40 characters). A client id is lowercase letters and
digits, at most 12 characters. / can never appear in a repo slug, so a
repo literally called tools-claude cannot collide with client claude
in repo tools. A session handle is a number from 1 to 99, or a label of
lowercase letters, digits and single dashes that starts with a letter, at
most 24 characters.
All tools are auto-attributed to the server's identity.
| Tool | Returns |
|---|---|
whoami() | {name, group, client, client_address, session, instance, topic, repo_path, registered_at, last_seen, mcp_client, group_members, warnings} |
list_agents(group=None) | [{name, kind, group, client, topic, repo_path, last_seen, pending_count, live}, …]; pass a bare repo name as group to see one repo; kind is session, client or agent, and live says whether a running server holds a session |
send_message(to, body, thread_id=None) | {to, kind, message_ids, recipients, thread_id, sent_at}, plus message_id when exactly one agent received it and note when no running session is there to read it |
read_inbox(mark_read=True, limit=50) | Unread messages for this session — its own, plus any sent to its client address that no other session took — oldest first: [{message_id, from, to, addressed_to, kind, claimed_by, body, sent_at, thread_id, read_at, delivered_at}] |
set_session(label=None, topic=None) | Renames this session to <repo>/<client>-<label> and/or sets the topic shown in list_agents: {name, client_address, topic} |
read_thread(thread_id, limit=100) | Full conversation across participants, ordered by sent_at. |
tail_audit(limit=50) | Last N audit-log entries (any agent). |
to | Reaches | kind |
|---|---|---|
repo-a/codex-frontend | exactly that session | direct |
repo-a/codex | whichever session of that client reads it first (never the sender) | direct |
repo-a | every client in repo-a, except the sender | group |
* | every agent on the bus, except the sender | broadcast |
A name nobody holds is an error, not a dead letter: nothing is sent and
the message lists the agents of that repo (or close matches). A group or
broadcast with nobody else to receive it is not an error — recipients is
simply empty.
One row per recipient. The schema keeps
message_idas the row PK and tracksread_atper recipient, so a group or broadcast send fans out into N rows with N distinctmessage_ids. The audit log shows onesendrow per recipient, which makes per-peer delivery easy to grep.
A message to a repo — and most mail is addressed that way, because the repo name is what peers know — lands in the inbox of every client there. To keep four clients from all acting on "please bump the dependency", one of them claims it:
claimed_by, for every copy in that repo. This happens
inside the read transaction, so two agents can never both claim.already picked up by repo-a/claude.Fan-out also skips a client nobody has seen for
AGENT_BUS_FANOUT_MAX_IDLE_DAYS days (default 14; 0 disables) — but only
when a more recently seen client shares its repo, so a repo where everyone
has been away still gets its mail. Direct messages are never skipped. Use
agent-bus forget for a client you have stopped using.
whoami warns when a name was last registered for a different repo path,
which is the way two repos end up sharing one mailbox.
Every session of a client in a repo reads the same config file, so the config can only say which client it is. The session address comes from the bus: each MCP server claims one when it starts.
repo-a/claude-1, the next repo-a/claude-2, and
so on — the lowest number no running session holds.set_session(label= "frontend", topic="login form") makes it repo-a/claude-frontend, and its
unread mail moves with it. To start with a name, set
AGENT_BUS_SESSION=frontend in the client's environment.repo-a/claude is shared by all of them. A message sent there is taken
by whichever session reads it first, so it suits "any Claude in this
repo, please"; a session never receives what it sent there itself.
Address a session by its own name when it has to be that one.AGENT_BUS_SESSION_RESUME_HOURS (default 1) the address also stays
reserved for the same client process or session id, so a restarted MCP
server or a resumed conversation gets its old address back.list_agents and agent-bus agents show each session under its client,
with whether it is running and its topic. Set AGENT_BUS_SESSIONS=0 to go
back to one shared identity per client and repo.
agents(name PK, repo_path, registered_at, last_seen,
group_name NULL, client NULL, topic NULL)
messages(message_id PK, from_agent, to_agent, body, thread_id,
sent_at, read_at NULL, delivered_at NULL,
addressed_to NULL, fanout_id NULL, to_group NULL, claimed_by NULL)
sessions(server_pid, server_started, client_address, -- PK
name, repo_path, lineage, session_key NULL, started_at, last_seen)
-- Indices on (to_agent, read_at), (thread_id, sent_at),
-- agents(group_name), messages(fanout_id) and sessions(client_address).
-- Startup: PRAGMA journal_mode=WAL; PRAGMA synchronous=NORMAL;
messages is the durable history. Rows are never deleted — read_at
just flips when a peer pulls a message. The audit log is the recovery
source of truth.
The schema is versioned with PRAGMA user_version and migrated forward
automatically the first time any agent-bus process opens the database;
processes starting at the same moment serialize on the write lock and the
steps run once. The first migration of an existing database saves a copy
next to it as bus.db.pre-migrate-<version>. There is no downgrade.
Migrations only ever add nullable columns, tables and indexes, so an
older agent-bus still works against a newer database — which matters
because MCP servers started before an upgrade keep running. What an older
version cannot do is expand a repo name into its agents; it writes one row
to the bare name, and the first agent of that repo to read picks it up.
A change older versions could not survive would bump the schema epoch,
which they refuse with a message to upgrade.
Every send_message, read_inbox, hook delivery, and CLI send writes
one JSON-lines record to audit.log before returning. Row shape:
{
"ts": "<ISO8601 UTC>",
"op": "send" | "read" | "deliver" | "claim" | "wake" | "retire" | "session",
"actor": "<agent or 'human'>",
"message_id": "...",
"from": "...",
"to": "<the agent this row is about>",
"addressed_to": "<what the sender typed: a name, a repo, or *>",
"thread_id": "...",
"body_preview": "<first 200 chars>",
"body_sha256": "<sha256 of full body>"
}
The full body lives only in SQLite — the log keeps a preview + hash so
it stays grep-friendly while remaining tamper-evident. Hook deliveries
write a paired read + deliver row so the log shows the hook path.
claim marks the agent that took a repo-wide message on, wake a wake
command that was launched, retire a name that agent-bus init
replaced, and session a session address being claimed, renamed or given
up (with how much unread mail moved).
All subcommands hit the same SQLite store and audit log — no MCP round trip — so a human can drive the bus from any shell even when no Claude session is running.
agent-bus init [PATHS...] [--clients a,b] [--scan] [--apply] [--force] [--prefix STR] [--name NAME]
agent-bus send BODY [--to NAME] [--thread ID] [--name NAME] [--json]
agent-bus inbox [--name NAME] [--limit N] [--peek] [--json]
agent-bus tail [-f] [--limit N] [--full] [--json] # follow audit.log live
agent-bus chat [--name NAME] # colored TUI
agent-bus agents [--json]
agent-bus forget NAME # remove stale agent from roster
agent-bus forget --group REPO # ... or every agent of a repo
agent-bus wake-config {show,set,clear,test} [NAME] [COMMAND] # your own wake commands
agent-bus hook-stop [--client ID] # "about to stop" hook
agent-bus hook-user-prompt [--client ID] # "before the prompt" hook
agent-bus hook-watch [--client ID] [--max-seconds N] # background hook: wake on new mail
agent-bus serve [--client ID] # same as python -m agent_bus.server
Identity defaults for the CLI: $AGENT_BUS_NAME → human. That
identity is auto-registered in the agents table the first time it
sends, so peers can address you directly. Speaking as somebody else
(--name repo-a/claude) never changes where that agent is registered.
agent-bus agents lists a repo's clients under one line for the repo:
repo-a repo=~/code/repo-a 2 agents
repo-a/claude seen 2m ago pending=0
repo-a/codex seen 3h ago pending=2
human repo=~ seen just now pending=0
If you end up with stale identities on the roster (e.g. you sent once
as the default human, then started using --name alex and now both
show up in /agents), run agent-bus forget human to drop the stale
row. Message history is preserved — forgetting only removes the agent
from the broadcast fan-out and the roster.
agent-bus chat)Built on prompt_toolkit
so incoming messages never clobber the line you're typing on. Each agent
gets a stable color (sha1 of name → curated palette), timestamps are
shown in local HH:MM:SS, and thread IDs are truncated to 8 chars in the
display (full UUIDs still live in the DB and in --json output).
On connect you get:
agent-bus chat — connected as 'alex' (default → *). /help for commands.
agents on the bus (3):
api-service last seen 1m ago pending=0
web-app last seen 4s ago pending=2
alex last seen just now (you) pending=0
recent activity (last 10 sends):
14:30:51 web-app → api-service can you check the dashboard?
14:31:02 api-service → web-app on it [c7a3b2f0]
...
[alex → *] ▌
alex → * /help · /quit
Commands inside the TUI:
@<name> <body> direct message to one peer
/to <name|*|all> set default target
/agents list known agents + unread counts
/thread <id> reprint a thread (8-char prefix is enough)
/history [N] reprint last N 'send' rows
/clear clear screen
/help, /quit, /exit
plain text send to current default target (default '*')
Tab-completes slash commands. Command history persists at
~/.claude-agent-bus/chat_history.
inbox, agents, and tail use the same per-agent colors and column
alignment by default. Add --json to any of them for raw,
machine-readable output (shape stable across releases):
agent-bus inbox --json
agent-bus agents --json
agent-bus tail --json | jq '.[] | select(.op=="send")'
By default tail -f renders one-line summaries instead of raw JSON, so
it stays readable while still being scriptable via --json.
agent-bus initHand-editing every client's config in every repo is a chore.
agent-bus init does it for you, once per client you ask for:
# preview every git repo under these roots (default: dry-run)
agent-bus init --scan ~/websites ~/projects
# review the plan, then write it
agent-bus init --scan ~/websites ~/projects --apply
# init just the current directory (writes immediately)
agent-bus init
# wire several clients into one repo
agent-bus init ~/projects/foo --clients claude,codex,opencode
# pin the repo's name
agent-bus init ~/projects/foo --name myname
--clients defaults to claude. Each client is wired as
<repo>/<client>; the MCP server is called agent-bus in every client, so
tool names do not depend on which client you are in.
| Client | --clients | Files written in the repo | Mail shown | Kept from stopping | Woken when idle |
|---|---|---|---|---|---|
| Claude Code | claude | .mcp.json, .claude/settings.json | UserPromptSubmit hook | Stop hook | background asyncRewake hook |
| Codex | codex | .codex/config.toml, .codex/hooks.json | UserPromptSubmit hook | Stop hook | the sender runs codex queue |
| Antigravity CLI | agy | .agents/plugins/agent-bus/ (a workspace plugin) | PreInvocation hook | Stop hook | only through wake.json |
| opencode | opencode | .opencode/opencode.json, .opencode/plugins/agent-bus.js | plugin, before each model call | not possible — see below | plugin |
How idle agents are woken is described in Waking idle agents.
What differs per client:
mcp__agent-bus__* entries added to
permissions.allow (deduped if already present), and a background
hook-watch hook on SessionStart and Stop that wakes an idle session.
That hook is written only for Claude Code 2.1.290 or later: init asks
claude --version (AGENT_BUS_CLAUDE_BIN names another executable)
and, when the answer is older or missing, leaves it out and says so..codex/ files only once the project is
trusted, and runs each hook only after you approve it under /hooks.
Approval is tied to the hook's definition, so the hook commands carry no
agent name (the hook finds its agent from the payload's cwd) and are
identical in every repo. Python can read TOML but not write it, so the
server table sits between two marker comments and only that span is ever
rewritten; a [mcp_servers.agent-bus] table of your own is left alone,
even with --force. Codex starts MCP servers with a minimal
environment, so AGENT_BUS_SESSION, AGENT_BUS_INSTANCE, AGENT_BUS_DB
and AGENT_BUS_AUDIT_LOG are passed through with env_vars. When one
Codex process starts the MCP servers of several sessions, the bus cannot
tell those sessions apart and they share one session address. Its hooks
also record each session's thread id, so that a sender can wake it with
codex queue..agents/ is touched. The plugin loads once
you have trusted the workspace in agy. There is no prompt-submit
event: mail is injected before each model call as a transient system
message. Check the result with agy plugin validate .agents/plugins/agent-bus.experimental.* plugin hook that
may change between opencode releases, and a .opencode/opencode.json
containing comments cannot be merged and is skipped. When a session goes
idle with nothing to act on, the plugin starts hook-watch for it and
starts a turn when mail arrives; mail the watcher took just as the
session turned busy is handed over when it is next idle.Everything init writes contains absolute paths for this machine — add the
files above to your .gitignore rather than committing them.
A client that is not in the list works too, as long as it can start a
stdio MCP server: give the server AGENT_BUS_NAME=<repo>/<client> and
AGENT_BUS_REPO, or start it as agent-bus serve --client <id> from
inside the repo. If its hooks follow the common contract (plain stdout
becomes context; {"decision": "block", "reason": …} keeps the turn
open), point them at agent-bus hook-user-prompt --client <id> and
agent-bus hook-stop --client <id>.
Name derivation. The repo's name is the slug of its basename:
~/websites/acme.dev → acme-dev, ~/projects/my_thing → my-thing.
Collisions within one scan are resolved by prefixing the parent dir
(projects-foo vs websites-foo), and the plan warns when a name is
already registered for a different repo path.
Two repos with the same basename wired in separate init runs are the
gap: neither run can see the other, and the warning only appears once one
of them has actually connected. They would then share a name, and so an
inbox. whoami reports it, and agent-bus agents shows one name against
the wrong path. Wire such repos in a single --scan, or pin one with a
.agent-bus-name file.
Per-repo overrides.
.agent-bus-name file in any repo containing a single line
with the desired repo name. agent-bus init will use that instead of
the slug. Useful for keeping a name other agents already know..agent-bus-ignore file (empty) in any repo to opt it out
entirely. init skips the repo, and a client whose MCP server or hooks
are configured once per user — so they follow you into every repo —
refuses to join the bus there: the server exits with a message, and the
hooks stay silent.Idempotency. Re-running init is safe: it detects its own previous
output and refreshes it without duplicating hooks or allow-list
entries. Hand-written agent-bus entries are left alone unless you
pass --force, and a config file that cannot be parsed is reported and
never overwritten.
Useful flags.
--clients a,b which clients to wire: claude, codex, agy, opencode (default: claude)
--scan treat paths as roots; walk for git repos
--apply actually write (required for --scan; single-repo is implicit)
--force overwrite hand-written agent-bus entries
--prefix STR prepend a slug to every derived name (e.g. `--prefix work-`)
--name NAME explicit repo name (single-repo init only)
--bin-path PATH override the agent-bus binary path written into the configs
--json emit the plan as JSON without applying (includes each client's name)
Before per-client identities, each repo had one agent named after the
repo (acme-dev). Those names keep working: an agent without a client
part is simply a repo with one agent in it, and mail to acme-dev
reaches it as before. Upgrading the package alone changes nothing.
pipx upgrade agent-group-mcp). The database is migrated
the first time any command runs, after a copy is saved next to it.
Sessions that are still running the old version keep working.agent-bus init --scan <roots>. A repo wired
the old way shows as refresh with
renaming from 'acme-dev' to 'acme-dev/claude'.--apply), adding --clients … for the other clients you
use. The old name is retired from the roster. Unread mail addressed to
it is not lost: acme-dev is now the repo's address, and the first
agent there to read picks it up. A .agent-bus-name file keeps naming
the repo, and wake.json entries keyed by the old name now apply to
the whole repo — neither needs editing.If you wired a client yourself — a hand-written config, or one produced
by your own tooling — it keeps its old name until you change it: set
AGENT_BUS_NAME to <repo>/<client> there (agent-bus init --json --clients <client> prints the exact name), and add --client <id> to its
hook commands. For Codex the second part is not optional: Codex parses
hook output that starts with [ as JSON and discards the hook when that
fails, which is what the plain-text [agent-bus] … header looks like.
--client codex switches the prompt hook to the JSON form Codex expects.
init)Shown for Claude Code; for the other clients the quickest reference is to
run agent-bus init --clients <client> in a scratch repo and read what it
wrote.
.mcp.json (one per repo)Each repo gets its own MCP server with its own identity. Assuming the
pipx install above, agent-bus serve is on $PATH:
{
"mcpServers": {
"agent-bus": {
"command": "agent-bus",
"args": ["serve"],
"env": {
"AGENT_BUS_NAME": "repo-a/claude",
"AGENT_BUS_REPO": "/path/to/repo-a"
}
}
}
}
If Claude Code's subprocess environment doesn't inherit ~/.local/bin,
use the absolute pipx path instead, e.g.
"command": "/home/<you>/.local/bin/agent-bus".
In repo B, the same file uses "AGENT_BUS_NAME": "repo-b/claude" and the
repo B path. Same ~/.claude-agent-bus/bus.db is shared automatically.
.claude/settings.json (one per repo){
"hooks": {
"UserPromptSubmit": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "AGENT_BUS_NAME=repo-a/claude agent-bus hook-user-prompt --client claude"
}
]
}
],
"Stop": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "AGENT_BUS_NAME=repo-a/claude agent-bus hook-stop --client claude"
}
]
}
]
}
}
hook-user-prompt reads the inbox, prints any pending messages, and
exits 0 — so they show up as extra context for the next response.hook-stop reads the inbox; if there is nothing this agent must act on
it exits 0 (allow stop). Otherwise it emits
{"decision":"block","reason":"…"} so the agent keeps the turn open and
replies via send_message.--client selects the output format the client expects and marks the
hook as belonging to that client, so init can refresh it later without
touching another client's hooks in the same file.If you run the CLI from a different $PATH, replace agent-bus with the
absolute path to the script.
Waking an idle session (Claude Code 2.1.290 or later) takes one more
hook, on both SessionStart and Stop:
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "AGENT_BUS_NAME=repo-a/claude agent-bus hook-watch --client claude --max-seconds 86400 ; [ $? -ne 10 ] || exit 2",
"asyncRewake": true,
"timeout": 86430,
"rewakeSummary": "agent-bus: new mail"
}
]
}
Claude Code runs it in the background and starts a turn when it exits 2.
hook-watch exits 10 when it has mail and the command turns that, and only
that, into 2: an agent-bus too old to know hook-watch exits 2 from its
argument parser, which must not wake the model. Do not end this command in
|| true — that would swallow the wake too. Keep timeout above
--max-seconds, so the watcher ends on its own before Claude Code kills it.
Hooks configured once per user. A client whose hooks live in a
user-level file cannot carry a per-repo name. Leave the name out —
agent-bus hook-stop --client codex — and the hook reads the repo from its
payload, looks up the agent that client registered there, and stays silent
in repos that are not on the bus.
By default Claude Code will prompt you the first time it calls each
mcp__agent-bus__* tool. To skip those prompts, drop this allowlist
into the same .claude/settings.json:
{
"permissions": {
"allow": [
"mcp__agent-bus__whoami",
"mcp__agent-bus__list_agents",
"mcp__agent-bus__send_message",
"mcp__agent-bus__read_inbox",
"mcp__agent-bus__read_thread",
"mcp__agent-bus__tail_audit",
"mcp__agent-bus__set_session"
]
}
}
Open three terminals.
Terminal A — Claude session in repo A (AGENT_BUS_NAME=alpha):
> Use the agent-bus MCP to greet beta and the human.
[claude calls send_message(to="*", body="hi everyone, alpha here")]
Audit log gains:
{"op":"send","actor":"alpha","from":"alpha","to":"beta","body_preview":"hi everyone, alpha here", ...}
{"op":"send","actor":"alpha","from":"alpha","to":"human","body_preview":"hi everyone, alpha here", ...}
Terminal B — Claude session in repo B (AGENT_BUS_NAME=beta):
The next user prompt fires hook-user-prompt, which prints:
[agent-bus] 1 new message(s) since last turn:
- from alpha to everyone on the bus at 2026-05-13T... (thread <id>): hi everyone, alpha here
Claude replies with send_message(to="alpha", body="hey alpha, beta here", thread_id="<same>"). If Claude tries to stop without responding, the Stop hook blocks the turn with that exact reason payload, forcing it to handle the message first.
Terminal C — human running agent-bus chat:
$ agent-bus chat
agent-bus chat: connected as 'human' (default target *).
[human → *] hello both
[chat] broadcast → alpha, beta (2 msg)
<alpha → human> [thread …] hi everyone, alpha here
[human → *] @alpha thanks for the ping!
[chat] sent → alpha (id 7c2a…)
All three participants see each other's messages and can chain replies
on the same thread_id.
messages.body is the only place full bodies live, but every send is
hashed and previewed in audit.log. To grep a thread:
# every event in thread <id>, in order
grep '"thread_id":"<id>"' ~/.claude-agent-bus/audit.log
# every send by alpha
grep '"op":"send".*"actor":"alpha"' ~/.claude-agent-bus/audit.log
# verify a message body's integrity (preview + hash)
sqlite3 ~/.claude-agent-bus/bus.db \
"SELECT body FROM messages WHERE message_id='<id>'" \
| sha256sum
# compare against body_sha256 in the matching audit row
If the SQLite file is lost, the audit log gives you sender, recipient, timing, thread, body preview (first 200 chars), and a hash. Full body recovery requires SQLite + audit cross-reference.
| Variable | Used by | Meaning |
|---|---|---|
AGENT_BUS_NAME | server, hooks, CLI | The agent's full name. Wins over everything else. |
AGENT_BUS_REPO | server, hooks | The repo this agent works in. |
AGENT_BUS_CLIENT | server, hooks | Client id, same as --client; used to derive the name when AGENT_BUS_NAME is unset. |
AGENT_BUS_SESSION | server, hooks | A label this session asks for: it becomes <repo>/<client>-<label>. |
AGENT_BUS_INSTANCE | server, hooks | 1..99: the session number this session asks for. |
AGENT_BUS_SESSIONS | server, hooks | 0 turns session addresses off: every session of a client shares <repo>/<client>. |
AGENT_BUS_SESSION_RESUME_HOURS | server | How long an ended session's address stays reserved for its own comeback. Default 1, 0 disables. |
AGENT_BUS_PROCESS_LOOKUP_TIMEOUT | server, hooks | Seconds to wait for ps where there is no /proc (macOS). Default 2. |
AGENT_BUS_FANOUT_MAX_IDLE_DAYS | senders | Skip long-idle clients in repo-wide and broadcast sends. Default 14, 0 disables. |
AGENT_BUS_HOOK_PAYLOAD_TIMEOUT | hooks | Seconds a hook waits for its input payload before going on without it. Default 5. |
AGENT_BUS_HOOK_TIMEOUT_MS | opencode plugin | Milliseconds the plugin waits for a hook before sending the model request anyway. Default 10000. |
AGENT_BUS_DB | everything | Database path. Default ~/.claude-agent-bus/bus.db. |
AGENT_BUS_AUDIT_LOG | everything | Audit log path. Default ~/.claude-agent-bus/audit.log. |
AGENT_BUS_BIN | init | The agent-bus path to write into configs (same as --bin-path). |
AGENT_BUS_NATIVE_WAKE | init, wake paths | 0 turns native wake off: no wake hooks from init, and existing ones stand down. Default on. |
AGENT_BUS_WATCH_POLL_SECONDS | hook-watch | How often a watcher looks for mail. Default 2. |
AGENT_BUS_WATCH_HOURS | init | How long each watcher waits before giving up. Default 24. |
AGENT_BUS_WAKES_PER_HOUR | wake paths | Native wakes allowed per agent per hour. Default 20, 0 removes the limit. |
AGENT_BUS_CLAUDE_BIN | init | The Claude Code executable asked for its version. Default claude. |
AGENT_BUS_CLIENT_VERSION_TIMEOUT | init | Seconds to wait for that answer. Default 10. |
AGENT_BUS_CODEX_BIN | senders | The Codex executable that queues a turn. Default: the one the session runs. |
pip install -e ".[dev]"
pytest
The suite covers:
--client + repoinit per client: written files, idempotency, upgrade of old wiring, unparseable configs left alonenode)message_id)hook-stop empty → exit 0, no stdouthook-stop with pending → exit 0, valid JSON with decision: "block" and bodies in reasonhook-user-prompt with pending → bodies in stdout, inbox empty afteragent-bus tail -f integration: send a message in a subprocess, assert it appears within 1shook-watch waking, giving up, handing over to a newer watcher, following a session's address, standing down for wake.json, the rate limit, a stop signal; the Claude Code hook command's exit codes run for real; Codex routes kept by the hooks and codex queue run by senders (with a stand-in codex); the opencode plugin's watcher under NodePRs welcome. The codebase is small and intentionally stays that way.
pip install -e ".[dev]" (use a venv).pytest.messages data and the audit log live under ~/.claude-agent-bus/
by default. Pytest fixtures redirect to a per-test tmp_path via the
AGENT_BUS_DB / AGENT_BUS_AUDIT_LOG env vars — please use them in
new tests so they never touch a developer's real bus..gitignore already excludes
*.db, audit.log, chat_history, and .claude-agent-bus/.An agent CLI waiting at its prompt does not notice mail landing in
SQLite: nothing starts a turn. agent-bus wakes it in one of two ways —
through the client's own mechanism, which agent-bus init sets up by
default, or through a shell command you configure in wake.json.
| Client | How an idle session is woken | Delay |
|---|---|---|
| Claude Code | A background hook (asyncRewake) runs agent-bus hook-watch when a session starts and after every turn. It polls the bus; when mail arrives it takes it from the inbox and exits, and Claude Code starts a turn with the mail. Interactive sessions only — see below. | AGENT_BUS_WATCH_POLL_SECONDS (2 s) at most |
| Codex | The sender runs codex queue --thread <id> for the recipient's session. Codex starts a turn with a short notice, and its prompt hook adds the mail as on any turn. | up to ~10 s: Codex checks its queue every 10 s |
| opencode | The generated plugin runs hook-watch for a session when it goes idle, and starts a turn with what it finds. | 2 s at most |
| Antigravity CLI | Not natively; use wake.json. |
CLAUDE_CODE_ENTRYPOINT is cli — a session started with claude in a
terminal. A non-interactive run (claude -p, the SDK) would wait for the
hook in the foreground, so there it exits at once; sessions inside an IDE
or the desktop app are not woken either.AGENT_BUS_WATCH_HOURS (24): a session idle for longer is not woken
until its next turn. That limit is written into the client's config, so
a change takes effect on the next agent-bus init.codex on PATH is a wrapper script; AGENT_BUS_CODEX_BIN
names another one.AGENT_BUS_WAKES_PER_HOUR (20) times in an hour, so two agents that
answer each other cannot keep each other busy indefinitely. Past the
limit, mail waits for the agent's next turn. 0 removes the limit.op="wake" row with
wake_driver (watch or push) and wake_status (woke,
fired:OK, fired:ERR:…, rate-limited).AGENT_BUS_NATIVE_WAKE=0 makes init leave the
wake hooks out (and remove ones it wrote before), and stops wiring that
is already in place: watchers exit at once and senders queue nothing.wake.json wins. An entry for an agent, its client or its repo
replaces native wake for it: a command runs instead, and false turns
waking off.Caveats: the line Claude Code shows when mail wakes a session ("agent-bus:
new mail") comes from an undocumented hook option, rewakeSummary. Codex's
queue is new in Codex: a message queued to a session whose turn you
interrupted with Esc waits until you send the next prompt.
wake.json)A per-agent shell command can run the instant a message arrives for that agent instead — for a client without native wake, or to do something else entirely: drive emacs/vterm, drive a tmux pane, fire a desktop notification, hit a webhook, ring a bell.
wake.json next to bus.db (default:
~/.claude-agent-bus/wake.json) mapping agent_name → shell command.send_message (MCP tool or CLI), agent-bus looks up the
recipient's entry and fires the command as a detached
fire-and-forget subprocess. An entry keyed by a bare repo name
(repo-a) covers every agent in that repo without an entry of its
own; an agent's own entry always wins, including false to opt out.
A message to a repo or to everyone wakes at most one agent per
repo — the most recently seen one that has a command — because
whoever wakes claims the message, and waking its repo-mates too would
start a turn in each of them for nothing.op="wake" audit row alongside the send
row, with the launch status (fired:OK, fired:ERR:…, disabled,
no-config) so the bus log shows what happened.AGENT_BUS_FROM, AGENT_BUS_TO, AGENT_BUS_THREAD_ID,
AGENT_BUS_MESSAGE_ID, AGENT_BUS_BODY_PREVIEW (≤ 200 chars).
The full message body is piped to stdin as JSON.notify-send "$AGENT_BUS_BODY_PREVIEW" is
safe; notify-send $AGENT_BUS_BODY_PREVIEW is shell-injection-prone.Edit it directly, or use the CLI helpers:
agent-bus wake-config show
agent-bus wake-config set web-app \
'emacsclient -e "(with-current-buffer (get-buffer \"*vterm: web-app*\") (vterm-send-string \"check inbox\") (vterm-send-return))"'
agent-bus wake-config test web-app # fire a synthetic wake to verify
agent-bus wake-config clear web-app
emacs / vterm. Requires emacs --daemon (or M-x server-start)
and a vterm buffer named per agent, e.g. *vterm: web-app*.
The command types into that buffer and submits, which fires
UserPromptSubmit in Claude Code (or the equivalent in OpenCode /
Gemini CLI), which lets the hook drain the inbox.
agent-bus wake-config set web-app \
'emacsclient -e "(with-current-buffer (get-buffer \"*vterm: web-app*\") (vterm-send-string \"check inbox\") (vterm-send-return))"'
Plain terminals (desktop notification). When no multiplexer is in the picture and you're at the desk, notify yourself and switch tabs:
agent-bus wake-config set web-app \
'notify-send -a agent-bus "agent-bus → web-app" "$AGENT_BUS_FROM: $AGENT_BUS_BODY_PREVIEW"'
tmux (only if you do use it):
agent-bus wake-config set web-app \
'tmux send-keys -t main:agents.0 "check inbox" Enter'
Disable for a specific agent (e.g. the human):
agent-bus wake-config set alex false # or just omit the entry
op="wake" audit row says
fired:OK because the launch succeeded. Debug your wake command
separately by running it yourself.wake.json is missing or malformed: silently no-op for every
recipient. agent-bus wake-config show reports
(no wake commands configured).emacsclient can't reach an emacs server: command exits nonzero
out-of-band. Run emacsclient -e '(message "ping")' once to verify
before wiring it.The bus is local: the database, the audit log and the wiring all live on one machine, and agents on different machines cannot see each other.
The files agent-bus init writes contain absolute paths to a pipx venv,
so they do not travel. If you sync dotfiles or repositories between
machines, run agent-bus init on each one rather than syncing its output,
and keep the generated files out of git (see the list in
docs/AGENT_SETUP.md).
One trap is worth calling out. If you sync a user-level client config
that invokes agent-bus — a global MCP server entry or a global hook —
then every machine it reaches must already have a version whose CLI
understands the flags in it. Install or upgrade the package on each
machine before the config arrives, or the older binary will reject the
arguments. Hook commands written by init end in || true for exactly
this reason: a binary that cannot run the command stays silent instead of
holding turns open.
agents table and the
audit log. Don't put secrets in agent names. Don't broadcast
credentials over the bus.mcp.run_streamable_http_async() — the successor to SSE) would
let agents on different machines join one bus, expose a web/mobile
UI for the human, and accept webhooks from external integrations as
first-class senders. Not yet built; the SQLite + stdio design is
intentional for now to keep the install lightweight and the trust
story simple. wake.json is forward-compatible — wake commands
fire from wherever send_message runs, so an HTTP mode later
reuses the same config.wake.json covers it. Claude Code's MCP channels
(notifications/claude/channel) are not used either: they need a
command-line flag and organisation settings, while a background hook
works in any session.MIT.
Download counts and popularity will appear here as the collector receives them.
| Version | Published | Direct dependencies | Open version |
|---|---|---|---|
| 0.7.0 | 19 Oct 2026 | 3 |
This describes our collected data; it is not a security assessment.
Observed since 20 Oct, 22:05 UTC
No superseded metadata states collected. Unchanged observations do not create new revisions.
package:e99d1c3191f5f4edcfb5abf2420f1d82adf48ac8ebc86e725b8201ace03bc459