Nested copper and steel gear assemblies inside a brass clockwork housing, lit with warm amber light.

Self-Hosting a Linear-Driven Claude Code Agent with Cyrus

Setting up a self-hosted, Linear-driven AI coding agent on my own server, and the two things that took actual work: keeping the bill sane and not handing it too much access.

I set up something I'd been meaning to get to for a while. There's an AI coding agent running on a Linux box at my place now that watches my Linear board and picks up whatever tickets I assign it. It makes a branch, does the work in a sandbox, and opens a PR. I hand it a ticket the same way I'd hand one to anyone else, and later there's a PR sitting there to review.

The runner is Cyrus . It's open source and drives Claude Code in headless mode. Getting it stood up was easy enough. The parts that took real time were keeping the bill under control and not giving an unsupervised agent more access to my repos than I was comfortable with. The upstream docs don't say much about either one. So this is what I ran into and how I dealt with it. The step-by-step with all the config is in a Gist at the bottom.

Why it's basically free

The short version: with subscription auth, this runs on the Claude plan you're already paying for. There's no separate per-token bill. It draws from your normal subscription usage, the same way interactive Claude Code does, so you're putting capacity you already have toward tickets while you do other things.

The billing here has been a moving target, so one note on where things stand. Anthropic announced that as of June 15, Agent SDK and claude -p usage would leave subscription limits and move onto a separate monthly credit. They postponed that on the morning it was supposed to land. For now nothing changes: agent and headless usage keeps running against your subscription as before, with no separate credit to claim, and they've said they'll give notice before any future change. Most of the write-ups out there still describe the credit as live today, so take those with a grain of salt and check your own account.

The real tradeoff right now is that a busy agent shares your subscription's rate limits. Time it spends grinding tickets is capacity you're not spending on your own interactive work. For me that's fine, since it mostly runs while I'm away from the keyboard.

What can cost you real money is billing to the wrong account. Claude Code can authenticate two ways. A subscription OAuth token from claude setup-token keeps you on your subscription. ANTHROPIC_API_KEY puts you on pay-as-you-go API pricing at full rates with no cap, and the key wins if both are present. There's a thread on the Claude Code issue tracker where someone ran scheduled jobs with claude -p and an API key and ran up around $1,800 in two days. So stay on subscription auth and keep stray API keys out of the environment.

Default the agent to Sonnet and save Opus for the tickets that need it, and it stays light on your limits.

What it runs

One Docker container. Linear gets to it through a Cloudflare tunnel I already had running, so I didn't expose anything new, just pointed one more hostname at a port on loopback. Ticket comes in, Cyrus clones the repo into a worktree for that issue, runs Claude Code against it in a sandbox, opens a PR. Two named volumes keep the state around so a restart doesn't wipe it.

One of those volumes is Claude Code's transcript directory, and you'll want to keep it. I didn't, the first time. Without it, restarting the container drops any session that was in progress, and the agent can't resume those tickets.

What tripped me up

Most of the time went into things the README leaves out. The bigger ones:

  • By default the Cyrus server binds to localhost inside the container, so the tunnel just returns 502s until you set the env var that makes it bind 0.0.0.0. The host still only publishes on loopback, so this doesn't expose anything.
  • The egress allowlist runs on bubblewrap, and bubblewrap won't start in a normal unprivileged container until you loosen two security options. Read up on what that changes before you do it. The guide lays out what you give up and what you keep.
  • If you've got a wildcard Cloudflare Access policy on the domain, it catches the Linear webhook and sends it to a login page, and the agent just sits there. You add a bypass for the webhook path.
  • If you don't set a model, the default is Opus, so an agent you haven't configured will run Opus on every little ticket and burn through the credit fast.

All of these are in the guide with the error you'll see and the fix.

Access

It's running code unsupervised, so I didn't want to be loose about what it can reach. The container is non-root and bound to loopback. The GitHub token is a fine-grained one scoped to just the repos I want it in. The sandbox sits behind an egress allowlist, so it can reach package registries and git and not much past that. Only I can assign tickets to it, which means nobody else in the workspace can spend the credit. Nothing sensitive lives on the box. I'd want all of that in place before pointing one of these at a repo that matters.

Is it worth running?

For tickets that are scoped well, yeah. A Linear issue that names the relevant files and says what done looks like usually comes back as a solid PR while I'm off doing something else. Vague tickets are where it wanders and spends money, which has had the side effect of making me write better tickets. I've got it set so the cheap read-only passes stay cheap and only the build work gets the wider tool access.

It won't replace anyone on the team. It's a sandboxed junior that works the queue overnight on the subscription I already pay for, so it isn't adding to what I spend. It just puts capacity I already have toward finished PRs while I'm off doing something else.

Full setup, the four config files, the security checklist, and the troubleshooting list are in the Gist: https://gist.github.com/devondragon/28fcf0077ff6f225f347da8f28093977

No comments yet