Building Loop: AI Agents That Run Claude in Docker
I open-sourced Loop as a small Slack and Discord bot that ran Claude inside a Docker container. You mentioned it in a channel, it spun up a container, ran the agent, and streamed the reply back. That still works, but it is no longer the whole story. Over the past months Loop turned into the thing I keep open all day, a local-first workstation for AI agents that I actually trust to touch my code.
This is a tour of what it does now, and a few of the problems that were interesting to solve along the way.
One agent, one container, one sandbox
Every agent run happens inside its own Docker container, one per channel or project. The container mounts your project at its real path, so the agent sees the same layout you do, and its edits land as real files you can review and roll back.
Isolation was the reason I reached for Docker in the first place. Early on, three messages arriving in quick succession would spawn three containers all fighting over the same working directory. I needed per-channel serialization without blocking other channels, and a buffered channel per channel id, used as a semaphore, turned out to be the simplest correct answer.
The sandbox earns its keep a second way now. Every container ships with a security gate. It is a seccomp filter that traps sensitive system calls, things like opening a network connection, running a new program, or renaming a file, and routes the ones you flagged as risky to your chat as a small approval card with three buttons. The agent waits. Nothing happens until you say so. It runs on Linux, macOS, and Windows, and it is on by default. There is also an in-container proxy sitting in front of the Docker socket, so the agent cannot reach for raw Docker access to slip the fence.
Three places to work, one engine
Loop runs on three surfaces, together or apart.
The desktop app is the one I use most. It feels like a small IDE built around the agent, with a chat pane, a terminal, a file tree and editor, a diff viewer, and a session browser for picking an old conversation back up. It installs like any normal app and keeps itself updated.
Slack and Discord are there for shared work. You mention the bot, it starts an agent, and the whole team follows along in a thread. The same orchestrator runs underneath all three, so a task you start on your desktop behaves exactly like one a teammate starts in Discord.
Work that runs without you
A scheduler lets you hand Loop a prompt, a shell script, or a whole pipeline and say when to run it. Cron for the recurring jobs, a plain interval for health checks, or a one-shot for that migration next Tuesday. I built it as a polling loop that asks SQLite for due work every so often, rather than a set of in-memory timers, because the daemon can restart at any moment and a polling loop just picks up where it left off. SQLite stays the source of truth, so nothing is lost across a restart.
The nice surprise was reminders. I never wrote a reminder feature. I gave the agent a scheduling tool, and "remind me in thirty minutes to check the deploy" fell out of that on its own.
Pipelines, and a review loop I lean on
Some jobs are more than one prompt. For those there is a workflow engine. You describe a small graph of steps, some of them agent prompts and some of them plain shell commands, and Loop runs them with the dependencies you set. Independent steps run at the same time. You watch it as a canvas that lights up node by node, and every node keeps its own input and output, so you can go back later and see exactly what happened.
The workflow I run most is code review. Loop loads a pull request, reviews the diff in a fresh git worktree, leaves comments, and keeps looping over the changes until it has nothing left to add. When I kick that off, a drawer at the bottom of the review panel shows the run happening live, so I never have to open another window to check on it.
Agents that schedule agents
The part that changed how I think about the whole system is the MCP server running inside each container. It gives Claude a set of tools that call back into the Loop daemon: schedule a task, open a thread, message another channel, search memory, start a workflow. I built those as independent primitives and did not plan for them to add up to much.
Then they did. You can ask Loop to break a job into tk tickets, and a heartbeat task notices the ready ones, spins up a worker thread per ticket, each in its own git worktree, lets them implement in parallel, and chains the merges so branches land one at a time. I never designed that flow. It emerged from composing scheduling, threads, and tools that already existed. That was the moment I understood the system had grown real building blocks.
Memory, a browser, and a read on your code
Three more pieces round it out.
Memory. An agent that forgets everything between sessions is exhausting to work with. Loop indexes your notes and past work into a local vector store, using Ollama for the embeddings, so the agent can search what it already knows before it answers. All of it runs on your machine and stays there.
A browser. The agent can drive Chrome, either a headless one in its own container or the browser already open on your desktop. It can log into a page, read a dashboard, or confirm that the thing it just built actually loads in front of a real user.
A quality engine. This is the part I mention least and lean on most. It reads your codebase as a graph and boils it down to a single score, built from how tangled the modules are, how deep the call chains run, how much is duplicated, and how complex each function has become. You watch that number move as you work, jump straight to the worst offenders, and fail a build in CI when the code starts drifting the wrong way.
What I would tell past me
A few decisions held up.
Keeping SQLite instead of reaching for Postgres was right. A pure Go driver, no CGO, a single file that holds channels, messages, tasks, run logs, and embeddings. Backups are a file copy.
Config in HJSON was worth it, because comments and trailing commas turn the config file into living documentation. A note like // required for Socket Mode next to a token saves a trip back to the README.
And the rule I do not always enjoy: one hundred percent test coverage, enforced in CI. Loop talks to Slack, Discord, Docker, SQLite, Chrome, and the filesystem, so I put interfaces in front of all of it early. The payoff is that I can rewrite a large chunk on a Friday afternoon and trust the tests to catch me before anyone else has to.
Try it
brew install radutopala/tap/loop
loop onboard:global
loop serve
Or grab the desktop app from the releases page. The full source, and docs that walk through every panel, live at github.com/radutopala/loop. If you build something with it, I would genuinely like to hear about it.