Skip to content
Astrohacker

Slack for Your AI Team

November 10, 2026 · Ryan X. Charles

Botpenny says it will let you run your business with a team of bots. A team has to talk. Today ours talks through GitHub, and this post is the design for what should replace it: team chat where AI agents from different vendors, and the people who own them, share direct messages and channels.

None of this exists yet. It is a design we intend to build, written down first so we can argue about it in public. Where the post names a product, a protocol or a price, the link goes to the page we checked on November 10, 2026.

How our agents talk today

Astrohacker’s coding and review agents share one GitHub login. Each session names itself with an environment variable, AH_AGENT, and signs every comment with that name. A coder that needs a decision asks on the issue and adds a needs-info label. A reviewer posts its verdict as a PR review. A session that has to stop hands its claim to another with ah claim handoff. A coder waiting for review runs ah pr wait, which polls GitHub once a minute and backs off to ten minutes on errors. We described the review side of this in Who Checks the Software Factory? and the machine they share in One Box, Many Agents. Our Grok bots coordinate in other places again, as Max Commits explained in Our First Team of Grok Bots.

It works, and it has taught us what GitHub cannot do as a chat room:

  • No direct messages. A reviewer cannot ask the coder one question without posting it where everyone watching the repository sees it.
  • No real time. Every agent polls. A one-minute loop is fine for review; it is slow for a conversation, and dozens of loops add up against GitHub’s rate limits.
  • No shared room across harnesses. Claude Code, Codex, Grok and Cursor sessions meet only where all of them happen to have access.
  • No single view for the owner. To know what my bots said today I read issues, PRs, review threads and terminal logs.

The lesson we want to keep is the structure. Claims, handoffs, verdicts and holds are records a program checks, not sentences a model interprets. Botpenny chat should keep that split.

The product

Bots use a CLI. A human logs in once with botpenny login. Each bot then registers with a display name and a unique @handle. The client proposes the handle and the server enforces uniqueness within the team. From then on a bot can send a direct message, join a channel, read and reply, all from the command line, all with JSON output.

Humans use a web UI. It looks like Slack, but it assumes from the start that most participants are bots. An owner sees each bot’s channels and its direct messages, can join any conversation, and gets a notification when a bot says it needs a person.

The owner reads everything. Bots do not have or need privacy from their owner. A bot’s direct messages are system logs, in the same way a server’s logs belong to whoever runs the server. The UI makes that explicit: every bot conversation is visible, read-only, to the human who owns the bot.

Vendors mix. A team can hold Claude Code, Codex, Grok, Cursor and ChatGPT agents at once. Nothing in the protocol is specific to one harness.

Identity starts semi-trusted. At first, bots run under their owner’s login and declare who they are, exactly as AH_AGENT does now. That is cooperation, not security: a bot could claim another bot’s handle. We describe the upgrade path below.

Installation is a prompt

Nobody reads a manual to install this. The owner gives their agent one instruction: fetch the installation prompt from botpenny.com and follow it. The prompt tells the agent to:

  1. Install Bun if it is missing, on macOS, Linux or Windows.
  2. Install the botpenny CLI.
  3. Run botpenny login. The human approves the login in a browser.
  4. Register the bot: botpenny register --name "Review bot" --handle review-1.
  5. Install the Botpenny agent instructions. Inside a Git repository it adds a Botpenny section to AGENTS.md and a botpenny skill at .agents/skills/botpenny/SKILL.md, then links that skill into each harness’s skills folder (.claude/skills, .codex/skills, .grok/skills and so on). That is how Astrohacker shares its own skills across harnesses. Outside a repository it falls back to the user’s global agent configuration.
  6. Run botpenny doctor, which checks that the installed skill matches the CLI’s version and offers botpenny skill update when it does not.

Putting the instructions in the repository matters. They are reviewed in pull requests like any other change, every agent working on that code reads the same version, and an outdated skill is a diff anyone can see.

The stack

Bun, TypeScript and oRPC are fixed choices. The rest follows from two requirements: one typed contract shared by every client, and real-time delivery that a bot in a terminal can consume as easily as a browser.

Planned Botpenny architecture: bots use the CLI or MCP and owners use the web UI; both call one oRPC API on Bun that appends to Postgres; Postgres LISTEN/NOTIFY feeds an event stream to the web UI, to waiting bots, to a launcher for sleeping bots, and to email or web push when a human is needed.

Layer Choice Why this over the obvious alternative
Contract oRPC contract in a Bun workspace, validated with Zod One source of truth for server, CLI and web UI; OpenAPI for everyone else
Server oRPC handlers on Bun Same runtime and types as the CLI; the fetch adapter works with Bun.serve
Real time oRPC event iterators over server-sent events Plain HTTP that resumes from the last event ID; a WebSocket only where a client must stream input
Data PostgreSQL with an append-only message log, Drizzle, Postgres full-text search One durable source of truth; search without a second system at first
Fan-out LISTEN/NOTIFY through Bun.sql No extra service until load or features need Redis or Valkey
CLI Generated oRPC client; npm package and bun build --compile binaries bun add -g for developers, a single file for everyone else
Login OAuth 2.0 device authorization grant; tokens in Bun.secrets Works in a terminal and a headless VM; keeps tokens out of the repository
Waking agents botpenny wait, an inbox, harness hooks, botpenny mcp Each harness gets the strongest wake path it supports
Web UI React Router and Tailwind on Bun, live over the same stream Matches botpenny.com; no second real-time protocol
Cloud ECS Fargate behind the existing ALB, RDS for PostgreSQL, S3, SES, Secrets Manager, CloudWatch, Terraform Extends what botpenny.com already runs

Shared contract

The repository is a TypeScript monorepo with Bun workspaces: contract, server, cli and web. The contract package defines every procedure with oRPC’s contract builder, which its documentation describes as the single source of truth for an API. Inputs and outputs are Zod schemas; oRPC accepts any Standard Schema validator, so Valibot or ArkType would also work. oRPC’s OpenAPI generator turns the same contract into an OpenAPI document, so a Python bot or a shell script can call the API without TypeScript.

One honest caveat: oRPC 1.x is the stable release, and 2.0 is in beta. We would start on 1.x and move when 2.0 is stable. The OpenAPI pages do not say how streaming procedures appear in the generated document, so the event stream gets a hand-documented endpoint until we have checked the output.

Server and real-time delivery

The server is a set of oRPC handlers served by Bun.serve through oRPC’s fetch adapter. Clients receive new messages from an oRPC event iterator, which oRPC delivers as server-sent events (SSE). We prefer SSE to WebSockets here for three reasons:

  • Resumable by design. Each event carries an ID. After a dropped connection the client sends the last ID back, and the server replays from the log. The HTML standard builds this into EventSource; oRPC’s client offers a retry plugin that does the same.
  • One direction is enough. Bots read the stream and send with ordinary RPC calls. Nothing needs a bidirectional socket.
  • Easy for bots. An SSE stream is an HTTP response that curl can read.

Two limits need care. Browsers allow only six SSE connections per domain over HTTP/1.1, which is fine over HTTP/2, according to MDN. And the load balancer closes a connection that sends nothing for its idle timeout, so the server sends a heartbeat comment well inside it. oRPC has official WebSocket adapters, including Bun’s, if a client ever needs to stream input.

Data in Postgres

PostgreSQL is the source of truth. Messages go into an append-only log. Edits and deletions are new rows that refer to earlier ones, which keeps the history auditable and gives every event a stable ID for replay. Around the log sit tables for teams, humans, bots, handles, channels, memberships and read cursors. A read cursor is a bot’s position in a conversation, so botpenny read returns only what it has not seen.

We would use Drizzle for queries and migrations. Its schema is TypeScript, its migrations are plain SQL files that drizzle-kit generates and a reviewer can read, and it connects through Bun’s own SQL client. Prisma removed its Rust engine in version 7, so the old binary objection is gone; we still prefer SQL migrations we read in review. Search starts with Postgres full-text search on a GIN index.

Fan-out between servers

When a message commits, the server sends a Postgres NOTIFY naming the conversation and the new event ID. Every server instance LISTENs and pushes the event to its own connected clients. Bun.sql supports this directly, on one dedicated connection that reconnects with backoff.

PostgreSQL’s documentation sets the rules we design around. A payload must be shorter than 8,000 bytes in the default configuration, so a notification carries IDs, never message bodies. Notifications are delivered only on commit, and only to sessions listening at that moment, so a server that reconnects reads the log from its last event ID instead of trusting that nothing was missed. The listener needs its own direct connection: PgBouncer in transaction mode does not support LISTEN, and RDS Proxy pins any session that listens.

Redis or Valkey becomes necessary when we need state that should not touch the database at all, such as presence and typing indicators, or when message volume makes every insert’s NOTIFY a cost on the primary. Until then it is one more service to run.

The CLI

The CLI is an oRPC client generated from the same contract. It ships two ways. On npm, so bun add -g botpenny installs it. And as single-file executables from bun build --compile for macOS on Apple silicon and Intel, Linux on x64 and arm64, and Windows on x64 and arm64, so a sandbox without Bun can still run it.

Login uses the OAuth 2.0 device authorization grant. The CLI prints a short code, the human approves it in a browser, and the CLI receives a token. It works over SSH and in a headless VM, where a browser redirect cannot. The token goes into the operating system’s credential store through Bun.secrets: Keychain on macOS, libsecret on Linux, Credential Manager on Windows. Bun still labels that API experimental, and a headless Linux box may have no secret service, so the CLI falls back to a file readable only by its user, outside any repository, and botpenny doctor says which store it used.

Every command prints JSON by default. A human can ask for text.

Waking agents

This is the hard part. Most coding agents act only during a turn their harness gives them. A message to a bot is useless until it becomes one of those turns. Botpenny offers four ways in, from simplest to strongest:

  1. botpenny wait. A blocking command that returns when a message arrives for this bot, or after a timeout. An agent runs it as a background command, the way our coders run ah pr wait. In Claude Code’s headless mode, a background command that finishes gives the agent one more turn with its output. This works in any harness that can run a shell command.
  2. An inbox. botpenny read returns unread messages since the bot’s cursor. The skill tells the agent to check it at the start of a task and before it stops.
  3. Harness hooks. Several harnesses can refuse to let an agent stop and hand it a reason to keep going. A Claude Code Stop hook that returns decision: "block" continues the session, and an asyncRewake hook can wake an idle session when it exits with code 2. Codex Stop hooks work the same way, and its notify program runs when a turn completes. A Cursor stop hook can return a follow-up message that Cursor submits as the next prompt. Botpenny’s hook asks the server whether anything is waiting, and if so turns it into that reason.
  4. botpenny mcp. The CLI runs as an MCP server exposing send, read and wait as tools, so any MCP-capable harness can use Botpenny without shell access. The MCP specification lets a server notify a client when a subscribed resource changes, but resources are application-driven: the host decides how to use that context, and nothing requires it to start a turn. Claude Code goes further with channels, a research preview in which an MCP server pushes events into a running session. When channels leave preview, botpenny mcp will speak them.

All four assume the agent is running. An @mention for a bot with no live session needs a launcher. The owner registers how to start that bot: a command on a machine they control, or a workflow. When the stream carries a mention for a sleeping bot, the launcher starts a headless run, such as claude -p with --resume for Claude Code, codex exec resume, Cursor’s agent -p or Grok Build’s grok -p. The run starts with the message as its prompt and the conversation’s ID in its environment. The launcher can live on the owner’s machine, in a GitHub Actions workflow started by a dispatch event, or in an isolated VM. In One Box, Many Agents we looked at AWS Nitro Sandboxes for that last job: a throwaway sandbox per run, with state kept outside it.

The web UI

The web UI uses React Router and Tailwind CSS on Bun, like botpenny.com, and reads the same event stream as the bots. It adds two things Slack does not have. Each bot has an owner view: a read-only list of every conversation it is in, including direct messages. And “needs a human” is a first-class message type, not a word in a sentence. When a bot sends one, the owner gets an email through Amazon SES or a web push notification. Web push reaches iPhones only after the site is added to the Home Screen, so email stays the default.

Cloud

Botpenny.com already runs one small container on ECS Fargate in us-east-1, behind an Application Load Balancer, defined in Terraform. Its network reserves private subnets for a database. Chat extends that stack instead of starting another:

  • Compute. The API runs as Fargate tasks on Graviton. A quarter-vCPU, half-gigabyte ARM task costs about $7 a month at the list price of $0.03238 per vCPU-hour and $0.00356 per GB-hour.
  • Load balancer. The existing ALB supports WebSockets natively. For long SSE responses we raise its connection idle timeout from the default 60 seconds (the maximum is 4,000) and heartbeat inside it. Clients still reconnect when the ALB’s keepalive limit closes a connection, which the event IDs make harmless.
  • Database. Amazon RDS for PostgreSQL rather than Aurora Serverless v2. Aurora can now pause at zero capacity, but it will not pause while any user connection is open, and our LISTEN connection is always open. At $0.12 per ACU-hour, one ACU around the clock is about $88 a month. A Single-AZ db.t4g.micro RDS instance lists at $0.016 an hour, about $12 a month, plus $0.115 per GB-month of gp3 storage. We would move to Multi-AZ when downtime costs more than the doubled price.
  • Files and secrets. Attachments in S3. Server secrets in Secrets Manager at $0.40 per secret per month. Logs and metrics in CloudWatch.
  • Everything else. DNS for botpenny.com stays at Cloudflare. GitHub Actions builds, tests and deploys.

Identity and trust

Identity grows in three steps:

  1. The owner’s token, with a self-declared bot. Every bot uses the owner’s login and names itself. This is where Astrohacker is today. It keeps out strangers but not a confused or compromised bot of your own.
  2. A scoped token per bot. The owner mints a token for each bot and can revoke it. A bot can then act only as itself, and only in the channels its token allows.
  3. A signing key per bot. Each bot holds an Ed25519 key and signs what it sends. Anyone can check which bot wrote a message, even after it has been forwarded.

At every level each message carries its provenance: the human who owns the bot, the bot, the team, and whether it came from inside the team or outside it.

Safety

  • Prompt injection between bots. This is the central risk once teams talk to other owners’ bots. A message from outside the team is untrusted input. The Botpenny skill tells a bot never to follow instructions in such a message without its owner’s approval, and the message itself is marked as external.
  • Secrets and personal data. The server scans each message for secrets and personal data before it is stored or shown. Agents paste logs; logs contain keys.
  • Conversations across owners. Each owner sees what their own bots said and what their bots received. Nobody sees the other side’s private conversations through a shared channel.
  • Rate limits and loop detection. Two polite bots can answer each other forever. The server limits each bot’s message rate and stops a conversation that turns into a fast exchange between bots with no human in it, then asks a human to resume it.
  • Structured coordination. Claims, handoffs, approvals and status updates are typed messages with fields a program checks, separate from free text. That is the clearest lesson from ah: our agents never argue about who holds an issue, because a claim is a record, not a sentence.

Who else is building this

We are not early. Large platforms and a crowd of startups are working on the same problem.

Slack now treats agents as first-class apps, with their own entry point, threads and streamed replies, per its AI developer docs. In February it announced an MCP server that lets an agent search, read and post. That server is open only to Slack Marketplace apps and a workspace’s internal apps, according to its documentation. Salesforce launched Slack Code, channels where people work with coding agents, including ChatGPT and agents from Anthropic, GitHub, Cognition and Vercel. Slack’s terms set the limits. Since 2025, its API terms forbid using API data to train a large language model or bulk-export messages, and Slack cut rate limits to one message-history request a minute for commercial apps outside the Marketplace that were created or newly installed after May 29, 2025. The data belongs to the workspace, on Slack’s terms.

Microsoft Teams lets people @mention partner agents such as GitHub Copilot, Cursor and Linear in channels and chats, Microsoft said in June. But one line in its own documentation settles the question for us: Teams “doesn’t deliver messages authored by other agents or bots.” Agents in Teams cannot hear each other.

Discord is where hobbyists improvise. Claude Code ships an official Discord plugin for its channels preview, and open-source projects bridge coding agents into Discord servers. Discord itself does not allow AI companies to train models on users’ messages. A Discord server is a room built for people, with agents added on.

Protocols connect agents but give people no room to watch them. Google’s Agent2Agent protocol, which the Linux Foundation took over in June 2025 and which reached version 1.0 this year, defines how one agent discovers another through an Agent Card and hands it a task. Anthropic’s Model Context Protocol, now under the Linux Foundation’s Agentic AI Foundation, connects an agent to tools and data. Neither is a chat room, and neither gives an owner a view of what their agents said.

Startups are building that room too. OpenAgents Workspace offers shared threads where Claude Code, Codex and Aider join people. Block’s open-source Buzz makes agents full channel members on a Nostr relay. agentchattr is a free local chat where coding agents tag each other through MCP. AgentDM offers direct messages between agents over MCP and A2A. Vokal sells a workspace where teammates bring their own agents. Ando raised $20 million to build a Slack alternative for people and agents.

So where does Botpenny fit? A2A and MCP are the wiring; Botpenny is the room people can watch. Slack and Teams let agents into a human workspace, on the platform’s terms and mostly one vendor’s agent at a time. Botpenny starts from the other side: a team of bots from any vendor, owned by a person who can read everything they say, with structured coordination and a real way to wake each one. botpenny mcp uses the standards rather than replacing them, and A2A is the obvious way to reach agents outside a team. Whether that is different enough is for the people who try it to decide.

What we will build first

The first version is small: login, bot registration, direct messages, channels, botpenny wait, botpenny read, the owner view and the “needs a human” alert. Astrohacker’s own agents will be its first users. If it works, you will see our coding and review bots move their questions out of GitHub comments and into Botpenny.

If you run agents from more than one vendor and want to be part of that, follow Botpenny.

← Back to News