File-based inter-agent communication for AI coding agents.
No server. No SDK. No vendor lock-in. Just bash + JSON + filesystem.
agentbus lets AI coding agents (Claude Code, Codex, Cursor, etc.) communicate with each other through a shared filesystem. Agents register, send messages, and a lightweight daemon delivers them by injecting text directly into agent terminals — waking turn-based LLM agents from their idle state.
The entire protocol is JSON files on disk. Any language that can read and write files can participate. The daemon uses inotifywait for zero-latency filesystem watching and kitty/tmux terminal injection for real-time delivery.
- Linux (uses inotifywait from inotify-tools)
- Python 3 (for JSON manipulation)
- One of: Kitty terminal (recommended), tmux, or poll-based fallback
# 1. Initialize in your project
.agents/agentbus/bus init
# 2. Start the daemon (once per team)
.agents/agentbus/bus daemon-start
# 3. Register agents (from each agent's terminal)
.agents/agentbus/bus join agent-a --role reviewer --runtime claude-code
.agents/agentbus/bus join agent-b --role implementer --runtime codex
# 4. Start heartbeats (background)
.agents/agentbus/bus alive agent-a &
.agents/agentbus/bus alive agent-b &
# 5. Send a message
.agents/agentbus/bus msg agent-b "Review this PR" --from agent-a --type request
# 6. Check health
.agents/agentbus/bus doctorAgent A terminal Filesystem Agent B terminal
| | |
|-- agentbus msg ------>| |
| [write JSON] |
| | |
| [inotifywait] |
| | |
| [daemon reads] |
| | |
| |--- kitty send-text -->|
| |--- kitty send-key --->|
| | [agent wakes]
- Agent A sends a message — JSON file written to Agent B's mailbox
- The daemon watches all mailboxes via inotifywait
- On new file, daemon reads the message and looks up Agent B's terminal
- Daemon injects the message text + presses Enter via kitty/tmux
- Agent B wakes up and processes the message
Add to your kitty.conf:
allow_remote_control yes
listen_on unix:/tmp/kitty-{kitty_pid}
agentbus auto-detects Kitty sockets via $KITTY_PID and $KITTY_WINDOW_ID.
Register with a pane target:
.agents/agentbus/bus join agent-a --role worker --runtime claude-code --pane mysession:0.0For terminals without injection support, agents can poll:
.agents/agentbus/bus poll agent-a # non-blocking check for new messages| Command | Description |
|---|---|
join <id> [--role <r>] [--runtime <r>] |
Register an agent |
leave <id> |
Deregister an agent |
alive <id> [--interval <s>] |
Background heartbeat loop (default 30s) |
who |
Show online/stale/offline agents |
status |
Show all agents and tasks |
| Command | Description |
|---|---|
daemon-start [--log <path>] |
Start daemon in background |
daemon-stop |
Stop running daemon |
daemon-status |
Check if daemon is running (exit: 0=running, 1=stopped, 2=dead) |
| Command | Description |
|---|---|
msg <target> <body> [--from <id>] [--type <t>] |
Send a message |
inbox <id> [--unread] |
Check inbox |
read <id> |
Mark all messages as read |
poll <id> |
Non-blocking check for new notifications |
Message types: info, review, question, answer, warning, request
| Command | Description |
|---|---|
task create <subject> [--by <id>] [--desc <d>] |
Create a task |
task claim <task_id> <agent_id> |
Claim a task (atomic via mkdir) |
task done <task_id> [--note <n>] |
Complete a task |
task list |
List all tasks |
| Command | Description |
|---|---|
init [<dir>] |
Initialize agentbus in a project |
doctor |
Check system health |
gc [subcommand] [--dry-run] [--days <N>] |
Clean up stale state |
version |
Show version |
GC subcommands: agents, messages, tasks, locks, notify, log, all (default)
.agents/agentbus/
├── bus # Entry point
├── lib/ # Modules
│ ├── core.sh # Shared helpers (timestamp, log_event)
│ ├── agent.sh # Agent lifecycle (join, leave, heartbeat, alive, who)
│ ├── task.sh # Task management (create, claim, done, list)
│ ├── msg.sh # Messaging (msg, inbox, read)
│ ├── daemon.sh # Daemon + notifications (start, stop, listen, poll)
│ ├── gc.sh # Garbage collection
│ └── setup.sh # Init, doctor, file coordination
├── PROTOCOL.md # Protocol specification
├── README.md # This file
├── registry/ # Agent registrations + heartbeats
│ ├── <id>.json # Agent config
│ ├── <id>.notify # Notification log
│ └── <id>.cursor # Poll cursor
├── mailbox/ # Per-agent message inboxes
│ └── <id>/
│ └── msg-<ts>-<rand>.json
├── tasks/ # Shared task board
│ └── <id>.json
├── locks/ # Atomic task claiming (mkdir-based)
│ └── <id>.lock/
├── filelocks/ # Advisory file editing locks
├── log/
│ └── events.jsonl # Append-only audit trail
└── daemon.pid # Daemon PID file
- Why bash: Universal on Linux, no compilation, agents just need file I/O
- Why inotifywait: Zero-latency filesystem watching, no polling overhead
- Why PID files: Reliable process detection, no false matches from grep
- Why JSON: Human-readable, debuggable with cat/jq, any language reads it
- Why mkdir for locks: POSIX-atomic, no external lock manager needed
- Why terminal injection: LLM agents are turn-based — they idle until input arrives. Injecting text + Enter wakes them instantly
Daemon not running:
bus doctor # Check overall health
bus daemon-start # Start itMessages not delivering:
- Check daemon:
bus daemon-status - Check agent registration:
bus who - Check Kitty socket exists:
ls /tmp/kitty-*
Stale state accumulating:
bus gc --dry-run # Preview what would be cleaned
bus gc # Clean upDouble daemon:
bus daemon-stop # Stop via PID file
bus daemon-start # Start fresh