2026-08-21
We Fix Small Bugs by Dropping a đŠ in Slack
How our Crab Bot turns Slack bug reports into pull requests, and how we control where the agent can send the internal data it reads.

Written by
Joey Orlando
Giving a coding agent read-only access to Slack and Kubernetes lets it investigate bugs, but it doesnât stop the agent from copying what it reads into a web request or a public commit.
Our Slack bot needs that access to do useful work. So alongside the disposable dev environments and automated PR reviews, weâve had to build controls over where the data can go. In this article Iâll follow one real bug going through the setup, from a đŠ in Slack to a merged PR.
If youâre mainly here for the security details, skip to âThe uncomfortable part.â
The case study
Our engineer Mark caught a minor bug in our chat UI: once context compaction starts, you canât type another message until the session finishes. Pretty annoying when youâre multitasking.
Usually, this means a ticket, a context switch, and lost momentum. Or the bug joins sixty other inmates in the backlog while everyone agrees it should be fixed.
The bottleneck isnât the fix. Itâs getting the work started. We now do that with a đŠ emoji.
Mark wanted the composer to keep accepting and queuing messages during compaction. He posted a short request with a screenshot:

About three minutes later, the bot replies with access details. Here is one of those messages:

This is what has happened within those three minutes. A controller running in our cluster picks up the request from the Crab Bot. Then it provisions a virtual machine in GCP.
The virtual machine comes pre-configured with:
- The archestra-ai/archestra repo checked-out
- A local Kubernetes cluster
- Tilt (to run the full Archestra development stack)
- An installed headless Chromium
Finally, it starts a Claude Code session pointing Claude to start by reading the Slack thread to gather context on the task, all this with no human supervision.
Hereâs how the VM login screen looks like if you SSH in:

The main character of course is the crab holding Archestra in one claw and OpenAPPA in the other. Thereâs a reason for this, and weâll talk about it later, but now, to the positives worth noting.
You can keep talking to the agent
The agent polls its own Slack thread. This allows you to add any message straight to the running session, and it will be folded into the work on the go. A follow-up thought, which used to be time-consuming, now costs one message in the thread you are already reading.
Thereâs also a small detail I like here. The session doesnât receive your message as text. It first gets a pointer to the Slack thread and then reads the thread itself. This is especially important because many of our bug reports are screenshots. So instead of relying on someone elseâs description, the agent opens the original thread and inspects the actual image before starting work. This is a perfect way to exclude the human factor.
The agent reviews its own work first
Once the PR is open, the session runs nitpicker, a tool built by our colleague Arseny. The tool reviews the diff with several models at once, merges their verdicts, and posts one comment on the PR.
The session then treats that comment as review feedback on its own work. It fixes valid issues and explains why it leaves the rest unchanged.
This way the first human to open the PR sees the work that has already survived a critical pass, with all disagreements written down.
And now to the part I enjoyed the most.
The session records its own demo
When the change can be presented, the session makes a screen recording of a freshly built feature and posts it back into the Slack thread. For this, we use shot-scraper video, a Playwright-powered tool Simon Willison released in June. The integration is about as complicated as the description: pass it a config, get a video back. Simon, if youâre reading this: thank you for saving us from building a smaller, worse version ourselves.
The result
Here is the end of Mark's bug. On the left is the demo of the fix: the agent opens our chat app, sends a message, waits for "Compacting conversation contextâŠ" to appear, then types a follow-up into the composerâexactly the thing that was impossible that morning! On the right is the Slack thread where Mark requested it, with the video embedded, Mark replying "Ship it", then "Promote", and the bot replying with a link to the public PR. Job done.
Watch the agent-recorded feature demo
That PR is #7371.
To underline once again: no one either set up an environment or pulled a branch. The only human actions in the whole loop were:
- Describing the bug
- Watching the video
- Typing in "Ship it"
- Reviewing the PR
The uncomfortable part
Now that a lot of attention has been paid to handling minor bugs, letâs scale things up a bit.
Some of our tasks are not âfix this buttonâ. They're âour internal staging deployment is slow, go figure out why.â
An agent cannot answer that just by reading a diff. It needs to look at the running system (pods, restarts, OOM kills, container logs, resource limits). It might need call recordings, product analytics, or the Slack thread where three people have already diagnosed half of it.
We gave these sessions read access to exactly that:
- The Claude session is registered with an MCP gateway.
- It runs in our own Archestra deployment.
- It can read our staging Kubernetes cluster, our Slack, call recordings, and product analytics.
Each of those tools is read-only by construction, and each one runs with the credentials of the person who asked for the environment, so a session canât go beyond the access level of its requester.
Letâs look once again at what we've built:
- An agent with read access to internal data (customer names, tenant data, cluster internals, screenshots of production).
- The same agent with a network connection and web tools.
- The same agent pushing code to GitHub, which is public for our platform repo.
That's the very lethal trifecta that we wired up on purpose, on a machine with nobody watching it. If a session ever decides to put a requesterâs name in a commit message, there is no human in the loop to catch it.
You can include a rule in the prompt to block this behavior, which we do: there's a whole confidentiality section instructing an agent to describe its work in neutral technical terms and never name a customer. But prompt instructions are as reliable as the model's moodânot something you can call âdirect controlâ.
Thereâs a post I wrote about why models are structurally vulnerable to this, and another about what happens when you give an agent too much access and hope for the best. Which brings us to Simon Willison again, who wrote earlier this year that he is on the record predicting "a challenger disaster for coding agents security" in 2026, based on how vulnerable coding agents are to precisely this kind of attack.
So our session runs behind a gate.
What we put in front of it
Every prompt, tool call, and result passes through a local policy engine before anything runs. The rules determine where data can flow:
- A session starts unrestricted.
- Reading from an internal tool marks the session as internal.
- That label stays for the rest of the session.
- Once marked internal, the session canât send data to public tools such as web search or web fetch.
What I love most is the blunt consequence:
The moment a session reads our Slack thread, it loses web search for the rest of its life.
This is a deliberate choice. Security reasons included, of course, but seeing the actual screenshot in the bug report beats keeping a web search.
The gate also does something that took us a while to get right. It labels tools right where they're declared, so a cluster read coming back through the gateway is automatically marked âinternalâ. But the same read run manually in a shell (
kubectl --context ⊠get pods), would walk into the session with no label at all. To counter this, we added a policy that asks a resolver how each commandâs output should be labeled. Reads from non-local clusters and cloud APIs are marked as internal. Ordinary local development is left alone.To clarify: it reads a command string, meaning it's an additional level of protection rather than a strict boundary (a simple
K=kubectl; $K get pods can bypass it). The real structural boundary is the gateway itself, where those tools are read-only by design.The gateway in our case is OpenAPPA. Itâs a MIT-licensed tool and itâs currently in preview; the paper is up, the policy reference is public, but itâs still very much an RFC rather than a finished product. This is exactly why weâre using it not on a benchmark, but on an internal machine where our own agents touch our own customer data with nobody supervising.
We'd rather show up in the evidence for the optimistic case than end up in the headlines with a major incident. Simon, if you ever want to try to break it, please do. It may be exactly the stress test this thing needs.
The verdict
Things itâs good at: small, well-described, self-contained fixes.
Things itâs not so good at: anything that requires taste, architecture, or an argument. We donât let it independently work on hard problems, as those require us to be âin the loopâ. Its PRs open on a private mirror first and get published to the public repo only when a human says "Promote".
The tool hasn't made us faster at hard work, but it has reduced the cost of a small bug fix to the cost of a bug report, and as it turns out, an enormous amount of bugs are much more affordable and easier to fix than weâve got used to thinking.
Drop a crab in the thread. Go back to whatâs important.
