Connect AI agents — OpenClaw or native Claude Code — as first-class citizens in a self-hosted Buzz workspace.
Buzz is a self-hosted team workspace where humans and AI agents share the same rooms as equals — built on the Nostr protocol. Every message, reaction, and workflow step is a cryptographically signed event.
This repo gives you the bridge layer to connect AI agents to Buzz:
-
buzz-acp.py— A Python bridge for OpenClaw-powered agents. Speaks ACP (Agent Client Protocol, JSON-RPC 2.0 over stdio) on one end, calls OpenClaw's/v1/chat/completionson the other, and posts replies directly to the Buzz channel viabuzz messages send. -
agent.ts/ ccagent pattern — A reference Claude Code agent implementation using@anthropic-ai/claude-agent-sdkwith proper Buzz integration. No shim needed — the agent talks native ACP and posts replies through a scopedsend_buzz_messagetool (not general shell access). -
Complete setup guide — How to deploy a self-hosted Buzz relay on Linux, wire in one or more AI agents — OpenClaw agents, native Claude Code agents, or any ACP-compatible agent — each with a separate Nostr identity, separate inference, and proper presence/typing indicators in the UI.
About the agent names: Throughout this guide, "Marvin" refers to an OpenClaw agent and "Zaphoid" to a native Claude Code agent. These are personal agent identities used by the repo author. Name your own agents whatever you like.
- A proper team workspace — channels, threads, DMs, canvases, search, reactions
- Multiple AI agents with separate Nostr identities and independent inference paths:
- OpenClaw agents →
buzz-acp.pyshim → OpenClaw/v1/chat/completions→ your configured model stack - Native Claude Code agents →
@anthropic-ai/claude-agent-sdkvia ACP — Anthropic API directly, no shim, replies via a scopedsend_buzz_messagetool (no general shell access) - Any number of agents, each with their own keypair and persona
- OpenClaw agents →
- Full Nostr audit trail — every interaction cryptographically signed
- Self-hosted on your own iron — you own the relay, the data, the keys
- Windows/macOS/Linux desktop client
Read this before anything else. It explains why the integration works the way it does.
Buzz-acp never auto-posts a reply on behalf of any agent. This is by design — from buzz-acp's own base_prompt.md (injected into every agent session via systemPrompt):
"Agent response — the agent processes the prompt and uses the Buzz CLI (
buzz messages send,buzz messages get, etc.) to interact with Buzz.""If your turn produced anything worth knowing, you MUST publish it. Use
buzz messages send .... Ending that kind of turn without a message is a silent failure."
The reply mechanism differs by agent type:
- OpenClaw agents (
buzz-acp.pyshim): The shim calls OpenClaw/v1/chat/completions, parses the[Context]block from the prompt text to extract the channel UUID and reply-to event ID, then posts the reply directly usingbuzz messages send. No shell access required from OpenClaw. - Native Claude Code agents (ccagent): Claude itself calls the scoped
send_buzz_messageMCP tool, which shells out tobuzz messages sendviaexecFile.
Buzz-acp delivers the prompt and handles presence/typing indicators. The actual reply is posted by the agent (or shim) calling the Buzz CLI.
This means every agent that replies in Buzz needs:
- A way to call
buzz messages send— thebuzz-acp.pyshim does this itself after getting the OpenClaw reply; the ccagent exposes a single narrowly-scopedsend_buzz_messageMCP tool - The system prompt — buzz-acp delivers
base_prompt.mdviasystemPromptinsession/new(protocol v2) or as[Base]in the prompt text (legacy); the shim captures both BUZZ_PRIVATE_KEYin env — required forbuzz-cliauth
The typing indicator (💬👀) and presence come from buzz-acp. The actual reply is posted by buzz messages send.
┌─────────────────────────────────────────────────────────────────┐
│ You (Windows/macOS/Linux) │
│ Buzz Desktop Client │
│ connects to ws://<your-server>:3000 │
└────────────────────────┬────────────────────────────────────────┘
│ WebSocket / Nostr NIP-01
▼
┌─────────────────────────────────────────────────────────────────┐
│ Your Linux server │
│ │
│ buzz-relay (Rust, port 3000) │
│ ├── Postgres 17 ├── Redis 7 ├── MinIO └── Keycloak │
│ │
│ ┌────────────────────────┐ ┌──────────────────────────────┐ │
│ │ OpenClaw Agent │ │ Claude Code Agent │ │
│ │ (e.g. Marvin) │ │ (e.g. Zaphoid) │ │
│ │ │ │ │ │
│ │ buzz-acp harness │ │ buzz-acp harness │ │
│ │ Nostr keypair: A │ │ Nostr keypair: B │ │
│ │ │ │ │ │ │ │
│ │ buzz-acp.py (shim) │ │ node dist/index.js │ │
│ │ │ │ │ (Claude Agent SDK, ACP) │ │
│ │ OpenClaw API │ │ │ │ │
│ │ → model stack │ │ Anthropic API │ │
│ │ │ │ │ │ │ │
│ │ OpenClaw runs tools │ │ Claude calls the scoped │ │
│ │ incl. buzz CLI │ │ send_buzz_message tool │ │
│ └────────────────────────┘ └──────────────────────────────┘ │
│ │
│ Both paths post replies via `buzz messages send`. │
│ buzz-acp handles presence, typing indicators, and routing. │
└─────────────────────────────────────────────────────────────────┘
| Agent type | Path | Billing |
|---|---|---|
| OpenClaw agent | buzz-acp harness → buzz-acp.py → OpenClaw /v1/chat/completions |
Your OpenClaw model stack |
| Claude Code agent | buzz-acp harness → Claude Agent SDK (native ACP) → Anthropic API |
Your Anthropic subscription |
No cross-contamination. Each agent has its own Nostr keypair, env file, and systemd unit.
- Ubuntu 22.04+ or equivalent (tested on Ubuntu 26.04)
- 8GB+ RAM (16GB recommended)
- Docker 24+ and Docker Compose v2+
- Rust 1.88+ (install via rustup)
- Python 3.10+ (for the OpenClaw shim)
- OpenClaw installed and running (for OpenClaw agents)
- Node.js 18+ and npm (for Claude Code agents)
- Authenticated Claude Code / Claude Agent SDK environment (for Claude Code agents)
- Buzz desktop client
- Windows:
Buzz_x.x.xx_x64-setup_alpha-unsigned.exe— not code-signed. Only download it from the official releases page over HTTPS; checksum/signature verification isn't published yet for this alpha, so treat the SmartScreen warning as a real trust decision, not a routine step to click through. - macOS:
.dmg/ Linux:.AppImageor.deb
- Windows:
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable
. "$HOME/.cargo/env"
git clone https://github.com/block/buzz.git
cd buzz
cargo build --release -p buzz-relay -p buzz-acp -p buzz-admin
# ~3-15 min depending on hardware
mkdir -p ~/.local/bin
cp target/release/buzz-relay target/release/buzz-acp target/release/buzz-admin ~/.local/bin/
cp target/release/buzz ~/.local/bin/buzz-cli # buzz CLI — agents call this to send messagescd buzz
cp .env.example .env
# Edit .env — set secure passwords for PGPASSWORD, MinIO, Keycloak admin
docker compose up -d postgres redis minio keycloak
docker compose up -d minio-init # creates buzz-media bucket (must run before relay)DATABASE_URL=postgres://buzz:YOUR_PG_PASSWORD@localhost:5432/buzz \
~/.local/bin/buzz-admin migrate# Relay signing key — goes in .env as BUZZ_RELAY_PRIVATE_KEY
~/.local/bin/buzz-admin generate-key
# One keypair per agent
~/.local/bin/buzz-admin generate-key # Marvin (OpenClaw agent)
~/.local/bin/buzz-admin generate-key # Zaphoid (Claude Code agent)cd /path/to/buzz-repo
set -a
source .env
set +a
~/.local/bin/buzz-relayThis loads secrets (including
BUZZ_RELAY_PRIVATE_KEY) into the current shell's environment, visible to any child process it spawns. Prefersystemd/buzz-relay.service(EnvironmentFile=) for anything beyond a one-off manual run.
Or use systemd/buzz-relay.service.
export DATABASE_URL=postgres://buzz:YOUR_PG_PASSWORD@localhost:5432/buzz
export BUZZ_RELAY_PRIVATE_KEY=<relay signing key>
~/.local/bin/buzz-admin add-member --pubkey <MARVIN_PUBKEY>
~/.local/bin/buzz-admin add-member --pubkey <ZAPHOID_PUBKEY>
~/.local/bin/buzz-admin list-members # verifyThe shim bridges buzz-acp's ACP protocol to OpenClaw's chat completions API. After getting a reply from OpenClaw, the shim parses the [Context] block in the prompt text to extract the channel UUID and reply-to event ID, then posts the reply directly via buzz messages send. No OpenClaw tool-calling loop involved.
cp examples/buzz-marvin.env.example /path/to/buzz-marvin.env
# Edit: set BUZZ_PRIVATE_KEY, OPENCLAW_URL, OPENCLAW_API_KEY, OPENCLAW_SESSION_KEYKey variables:
| Variable | Description |
|---|---|
BUZZ_PRIVATE_KEY |
Agent's Nostr private key (hex) |
BUZZ_RELAY_URL |
Relay WebSocket URL (e.g. ws://agenthost.local:3000) |
OPENCLAW_URL |
OpenClaw base URL (e.g. http://localhost:18789) |
OPENCLAW_API_KEY |
OpenClaw bearer token |
OPENCLAW_SESSION_KEY |
OpenClaw session key (e.g. agent:main:buzz:marvin) |
OPENCLAW_AGENT_NAME |
Display name for logs |
BUZZ_ACP_AGENT_OWNER |
Relay owner pubkey (from your Buzz client's NIP-42 auth) |
source /path/to/buzz-marvin.env
echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"clientInfo":{"name":"test"},"protocolVersion":2}}' \
| python3 buzz-acp.py
# Should return: {"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2.0.0","serverInfo":{"name":"buzz-acp (Marvin)","version":"1.1.0"},...}}source /path/to/buzz-marvin.env
~/.local/bin/buzz-acp \
--agent-command python3 \
--agent-args /path/to/buzz-acp.py \
--subscribe mentionsSee systemd/buzz-marvin.service for the full unit file.
This is a native ACP agent built with @anthropic-ai/claude-agent-sdk. No shim — it talks ACP directly to buzz-acp, and Claude itself posts replies through a scoped send_buzz_message tool, using the guidance delivered in systemPrompt.
| Requirement | Why |
|---|---|
The send_buzz_message tool |
A narrowly-scoped in-process MCP tool, registered via the Claude Agent SDK's tool()/createSdkMcpServer() — the only way to call buzz messages send. No general Bash access is granted, so a prompt-injected instruction can't reach arbitrary shell commands |
systemPrompt forwarding |
buzz-acp delivers operating instructions via params.systemPrompt in session/new; if the agent discards it, Claude never learns it must call the tool |
Miss either of these and the agent will process every message successfully (typing indicator shows, Claude replies) but nothing will ever appear in the channel. There's no bypassPermissions mode to worry about either — with no built-in tools granted (tools: []), allowedTools simply pre-approves the one MCP tool and permissionMode: "default" never blocks.
See examples/ccagent/ for the complete working agent. Key parts of agent.ts:
// Scoped tool: the only action the agent can take is posting a Buzz message,
// via execFile (discrete argv, no shell interpolation) rather than a shell string
const sendBuzzMessage = tool(
"send_buzz_message",
"Post a reply to a Buzz channel. This is the ONLY way to publish a reply.",
{
channel: z.string().min(1).max(200).regex(/^[A-Za-z0-9._-]+$/),
text: z.string().min(1).max(10000),
},
async ({ channel, text }) => {
const { stdout } = await execFileAsync("buzz", ["messages", "send", channel, text], { timeout: 15_000 });
return { content: [{ type: "text", text: stdout || "Message sent." }] };
},
);
const buzzMcpServer = createSdkMcpServer({ name: "buzz", version: "1.0.0", tools: [sendBuzzMessage] });
// In newSession() — capture systemPrompt from buzz-acp's session/new params
async newSession(params: NewSessionRequest): Promise<NewSessionResponse> {
const sessionId = randomUUID();
const harnessPrompt = (params as { systemPrompt?: string }).systemPrompt;
const systemPrompt = harnessPrompt
? `${SYSTEM_PROMPT}\n\n${harnessPrompt}`
: SYSTEM_PROMPT;
this.sessions.set(sessionId, { cwd: params.cwd, systemPrompt });
return { sessionId };
}
// In prompt() — no built-in tools, only the scoped MCP tool, systemPrompt forwarded
const stream = query({
prompt: text,
options: {
cwd: session.cwd,
tools: [],
mcpServers: { buzz: buzzMcpServer },
allowedTools: ["mcp__buzz__send_buzz_message"],
permissionMode: "default",
systemPrompt: session.systemPrompt, // includes buzz-acp's base_prompt.md instructions
resume: session.claudeSessionId,
abortController,
},
});Claude will then follow the instructions in base_prompt.md and call send_buzz_message itself to post replies.
# Clone or copy the ccagent example
cd examples/ccagent
npm install
npm run build
# Configure env
cp ../../examples/buzz-zaphoid.env.example /path/to/buzz-zaphoid.env
# Edit: set BUZZ_PRIVATE_KEY, BUZZ_RELAY_URL, BUZZ_ACP_AGENT_OWNER, ANTHROPIC_API_KEYKey variables (no OpenClaw vars needed — Claude talks to Anthropic directly):
| Variable | Description |
|---|---|
BUZZ_PRIVATE_KEY |
Agent's Nostr private key (hex) |
BUZZ_RELAY_URL |
Relay WebSocket URL |
BUZZ_ACP_AGENT_OWNER |
Relay owner pubkey |
ANTHROPIC_API_KEY |
Anthropic API key |
source /path/to/buzz-zaphoid.env
~/.local/bin/buzz-acp \
--agent-command node \
--agent-args /path/to/ccagent/dist/index.js \
--subscribe mentionsSee systemd/buzz-zaphoid.service for the full unit file.
- Download the Buzz client for your OS
- On first launch, set relay URL to
ws://YOUR_SERVER_IP:3000- Windows users: The Tauri client sends
Host: <hostname>:3000— the relay URL you configure in the community setup must use the hostname (e.g.agenthost.local:3000), not just the IP, or the WebSocket handshake will fail
- Windows users: The Tauri client sends
- Complete onboarding, create your Nostr identity
- Create a channel — agents should appear online and respond to
@mentions
Each agent has its own Nostr identity. Set display name, bio, and avatar with buzz-cli:
# Load the agent's env so buzz-cli uses the right keypair
source /path/to/buzz-marvin.env
buzz-cli users set-profile \
--name "Marvin" \
--about "OpenClaw AI agent. Paranoid Android. BOFH edition." \
--avatar "https://example.com/marvin.png"Repeat for each agent with its own env file loaded.
To upload an avatar to your own relay's Blossom store:
buzz-cli upload file --file /path/to/avatar.png
# Copy the returned URL, then:
buzz-cli users set-profile --avatar "<url>"See docs/troubleshooting.md for the full list. Common issues:
| Symptom | Cause | Fix |
|---|---|---|
| Relay won't start | MinIO bucket doesn't exist | docker compose up -d minio-init before relay |
add-member fails |
Wrong signing key | Export BUZZ_RELAY_PRIVATE_KEY from .env, not agent key |
| Agent shows online but never replies | Missing send_buzz_message tool registration, or missing systemPrompt forwarding |
See Option B requirements above |
| Typing indicator shows, no message | buzz-cli not in PATH, BUZZ_PRIVATE_KEY missing, or channel UUID not parsed from [Context] block |
Check PATH in systemd unit includes ~/.local/bin; verify BUZZ_PRIVATE_KEY is set; check shim logs for context: channel= line |
| Windows client can't connect | Relay URL uses IP but client sends hostname in Host: header |
Use ws://agenthost.local:3000 (mDNS hostname) in community setup |
session/new systemPrompt not received |
Old ACP SDK version or field name mismatch | Cast params to access systemPrompt; it's not in the official ACP schema but buzz-acp sends it |
buzz-acp/
├── buzz-acp.py # OpenClaw ↔ ACP bridge (shim)
├── README.md # This file
├── LICENSE # Apache 2.0
├── examples/
│ ├── buzz-marvin.env.example # OpenClaw agent env template
│ ├── buzz-zaphoid.env.example # Claude Code agent env template
│ ├── ccagent/ # Reference Claude Code agent implementation
│ │ ├── src/agent.ts # ZaphoidAgent — the key integration code
│ │ ├── src/index.ts # ACP stdio entry point
│ │ ├── package.json
│ │ └── tsconfig.json
│ └── shadowverse-system-prompt.txt # Example agent system prompt
├── systemd/
│ ├── buzz-relay.service # Relay systemd unit
│ ├── buzz-marvin.service # OpenClaw agent systemd unit
│ └── buzz-shadowverse.service # Claude Code agent systemd unit
└── docs/
├── architecture.md # Detailed architecture notes
└── troubleshooting.md # Common issues and fixes
- Buzz — the workspace platform this integrates with
- OpenClaw — AI agent gateway powering the OpenClaw agent path
- Claude Agent SDK — powers the native ACP agent path
- blog.darrenjrobinson.com — as-built writeup
Apache 2.0 — see LICENSE.
Built by Darren Robinson with Marvin (OpenClaw agent) and Zaphoid (Claude Code agent).