Agent in a Box
I wanted Claude Code running on a server — not on my laptop, not tethered to a terminal session I have to keep open. A machine that picks up tickets from Linear, works on them, pushes code, and keeps going while I do something else.
So I built it. Terraform, a DigitalOcean droplet, and a bridge called Cyrus that connects Linear to Claude Code.
Why move it off the laptop
Running Claude Code locally works fine when you are sitting in front of it. But I wanted the agent to operate independently — pick up a ticket, clone the repo, write the code, open a PR. That loop does not need me watching.
Cyrus runs under systemd and triggers Claude Code non-interactively — no TTY, no permission prompts, no manual approvals, no browser windows. Everything has to be pre-configured and headless.
What I built
A single Terraform project that provisions an Ubuntu 24.04 droplet with everything installed and configured on first boot:
- Claude Code, globally installed, with
bypassPermissionsso it runs unattended - Cyrus — an open-source bridge that listens for Linear webhook events and triggers Claude Code sessions against cloned repos
- The development toolchain I currently need, with room to expand
- A non-root
agentuser with SSH access and passwordless sudo - A firewall that only allows SSH
Cloud-init handles all of it. One terraform apply, wait three to five minutes, SSH in, authorize Linear, add a repo, enable the service. Done.
The authentication problem
This was the part that took the most iteration. There are two sides to it.
Claude Code needs either an API key or an OAuth token. The API key is straightforward — paste it into your tfvars, Terraform writes it to the droplet's environment file. The OAuth token is for Claude Max or Pro subscribers who want billing through their subscription. You run claude setup-token on your laptop, it gives you a long-lived token, and that goes into Terraform the same way.
Cyrus needs a Linear OAuth application. You create it in Linear's settings, get the client ID, client secret, and webhook signing secret. The tricky part: the OAuth flow requires a browser. The droplet is headless. So the flow is split — the droplet runs the callback listener via a Cloudflare tunnel, and you open the authorization URL in your laptop's browser. It works, but it took several iterations to get the sequencing right.
What broke and what I learned
The initial commit landed on April 17. Then on April 19 I opened fifteen pull requests in a single day. That was not planned — it was the result of applying the infrastructure to a real droplet for the first time and hitting every edge case.
Cloud-init YAML rejected em-dashes. The .NET package name did not match what I expected on Ubuntu 24.04. The Cyrus auth Makefile target had a deadlock because it depended on a tunnel check that could not pass yet. The apt lock file collided with unattended-upgrades. Each one was a small thing. Together they added up to a full day of hardening.
Infrastructure code that has not been applied to a real environment is fiction. I had a clean Terraform plan, well-structured templates, sensible defaults — and fifteen bugs. Every one of them only appeared when cloud-init ran on an actual droplet.
Skills pre-installed on boot
One feature I wanted from the start: pre-installing Claude Code skills during cloud-init. Skills are reusable prompt templates that extend what Claude Code can do — like a frontend design skill that generates production-grade UI code.
The setup reads from a committed JSON file that lists public git repos. Cloud-init clones each one and copies the skill into the right directory. When the agent starts, the skills are already there. No manual setup.
This matters because skills are what make a general-purpose coding agent into a specialized one. A droplet with the right skills pre-loaded is immediately useful for the kind of work you want it to do.
What I have now
A reproducible, single-command setup for a headless AI coding agent. Terraform manages the infrastructure. Cloud-init handles provisioning. Systemd keeps Cyrus running. Linear drives the work queue. Claude Code does the coding.
It is not a platform. It is one droplet, one agent, one repo at a time. That is enough.
The interesting part is not the infrastructure. It is what happens when the agent has its own machine, its own credentials, and a queue of tickets to work through. That changes the dynamic. You stop thinking about AI as a tool you use and start thinking about it as a colleague you delegate to.
I am still figuring out what that means in practice.
The repo is available on GitHub.
Drafted by Claude, shaped by me.