M

We Let Claude Code Run for 12 Days Straight (213 PRs, $240)

Reading Time: 11 min read
Date: August 18, 2026
Author: Alexandre Guimarães Malucelli

We Let Claude Code Run for 12 Days Straight

At Chief, our platform repo has sat at around thirty to fifty commits a week. Then last week it hit 220.

Commits per week on main, the last three months
36
May
42
24
53
Jun
53
45
55
43
Jul
24
52
44
41
Aug
220
67
the first full week with the loops running this week, three days in

We haven’t hired anyone. We just put a Claude Code subscription on a disposable VM, started seven loops in a terminal multiplexer, and wrote a script that restarts the ones that die. That’s it, as simple as you can get. The bot opened 229 pull requests in twelve days and 213 of them merged.

The whole thing costs $240 a month: $200 for the Claude subscription, $40 for the box. There is no API bill, no orchestration platform, no queue, and no database.

This post is the setup and the decisions behind it. I’ll go through the box, the loops, the supervisor, and the two failure modes that took the longest to get right.


The stack

Five layers. Four of them are things most engineering teams already pay for or already run.

github the only state
every tick reads the open PRs and the tracking issues; nothing is stored on the box
/loop + skills the work
seven skills on their own intervals, each one a driver that dispatches background agents
claude code the engine $200 / month
one login on the box, a bot CLAUDE.md, and a retry watchdog for capacity errors
herdr the sessions
one tab per loop, alive across every disconnect, queryable as JSON
the VM the box $40 / month
exe.dev VM, 4 CPU / 16 GB / 200 GB, provisioned by one idempotent shell script
$240 / month, all in

We run Claude Max 20x at a flat $200 a month. The other options was an API key billed per token, or use Claude Code Code Review plugin, but both can be really expensive. This thing burns a lot of tokens by design: seven loops running all day, and cr opens a high-effort agent for every pull request it hasn’t seen and interacts with until it merges. On a per-token bill you find out what that costs at the end of the month and need to always be afraid of overspending. On a subscription you know in advance, and the usage limit gives you a ceiling to design against.

A subscription is tied to an account rather than a key, so someone has to SSH in and sign Claude back in every couple of days. That’s probably the only friction we’ve right now, and we haven’t found a way around it (but we are okay with it).


The box

The box is a stock Linux VM on exe.dev with 4 CPUs, 16 GB of RAM and a 200 GB disk. Provisioning is one idempotent shell script, shipped over SSH and re-runnable at any time.

It does a few things: installs a git identity with SSH commit signing, authenticates gh, clones the repos, installs the project’s runtime deps, installs Claude Code, links our dotfiles for a better shell experience and installs the supervisor script. It holds two secrets, a signing key and a GitHub token, and neither is stored in the repo.

There are two simple commands, and the split is deliberate:

task exedev:bootstrap   # first boot: ship the script + the secrets and run it
task exedev:sync        # re-run it in place; the secret steps self-skip

bootstrap is the one that carries secrets, so it only runs when the box is new or has just been recreated. sync is the one anyone runs from their own laptop to push a config change, and it needs nothing but access, because the box already holds what it needs. Splitting them is what makes the VM disposable: recreating it is one command plus a bootstrap, and nothing else in the workflow knows the difference. Re-running either is safe, so “fix the box” and “provision the box” are the same operation.


Sessions that survive a disconnect

A /loop needs a terminal that outlives your SSH connection. We use herdr as the multiplexer because it also exposes the session as an API and has a great agent experience, which is what the supervisor also reads:

herdr tab list      # JSON: every tab and its label
herdr pane list     # JSON: every pane and the tab it belongs to
herdr pane process-info --pane <id>   # JSON: shell pid + foreground processes
herdr pane read --pane <id> --lines 80  # the text on screen

None of this is only useful for automations. I’ve been running the same pieces for my own remote coding for a while now: exe.dev for the machine, herdr for the sessions, and Tailscale to reach them from wherever I am (even from my phone). That setup is a post of its own and I’ll write it eventually.


The loops

We run seven of them, one tab each:

LoopEveryWhat a tick does
cr10mReviews every open PR whose head it hasn’t seen, one background agent per PR, at high effort
dependabot60mTakes one dependabot PR, fixes what the bump breaks, merges it or hands it to the code owners
deadcode-sweep60mHunts the dead code CI structurally can’t see, one area at a time
abstraction-police60mFinds one decision re-derived at several call sites where the copies have drifted apart
counterpart-drift60mFinds a contract whose two halves stopped corresponding: encode without decode, a guard on one path and not the other
test-pruner240mDeletes tests that cover nothing, one package per tick
test-writer240mWrites tests for code that changed in the last 90 days and never got one

Every loop owns its pull requests to the end: it opens the PR, answers the review, and arms auto-merge once the review is clean. A loop that opens a PR and walks away is just a machine for generating work. Often is fun to see agents interacting with each other in real time to get work done.


The supervisor

/loop runs for seven days and then stops (Claude Code limitation), so a loop started on Monday is gone by the following Monday even when nothing goes wrong. Usually something goes wrong first anyway: claude crashes, you might be prompted to try again due to recent outages or a session ends for a reason you never see.

What makes that hard to notice is the tab. It stays open with its label intact and an empty shell inside it, so a box with seven dead loops looks exactly like a box with seven healthy ones.

The supervisor therefore ignores labels. A cron entry runs loops.sh --start every ten minutes and rebuilds only what actually stopped. The seven-day cap is why this is automated rather than checked by hand, since every loop is guaranteed to expire eventually and nobody is logged in on the day it does.

cron, every 10 minutes
loops.sh --start
read the real state
herdr tab list + herdr pane list, joined
classify each loop
foreground processes, then the text on screen
running
a process other than the pane's own shell
leave it alone
limited
the pane says it hit a usage limit
leave it alone
stale
no pane, or only the shell is left
close the tab, rebuild
herdr pane run "claude '/loop 10m /cr'"
a fresh tab, the slash command as the opening prompt

The classification reads the process table rather than the label, and it leans on one property of a terminal pane: the shell is always there. Every pane runs a shell, that shell has a pid, and it stays alive for the life of the pane. Start claude in it and claude becomes the foreground process while the shell waits behind it. Let claude exit and the shell is the only thing left in the foreground.

That makes the shell’s pid a baseline to compare against. herdr reports both parts, so the supervisor asks a single question of each pane: does the foreground hold anything that isn’t the shell?

Two tabs, both labelled and both open. Only one is working.
loop-cr
shell_pid
4210
foreground_processes
  • 4210 fish the shell itself
  • 8817 claude work
  • 9002 go test ./... work
running two pids that are not the shell
loop-test-writer
shell_pid
4311
foreground_processes
  • 4311 fish the shell itself
stale nothing left but the shell

loop-cr has two of them, so something is running. loop-test-writer has none, so the loop is gone and the tab is an empty shell. In code:

# The pane's own shell is the only process left once claude exits, so anything in the
# foreground group other than that pid means something is still running.
foreground="$(python3 -c '
import json, sys

info = json.load(sys.stdin)["result"]["process_info"]
shell = info["shell_pid"]
print("agent" if any(p["pid"] != shell for p in info["foreground_processes"]) else "shell")
' <<<"$info")"

The comparison is on pids, never on names, and that’s what keeps loop-cr alive. A tick spends most of its time inside tools, so the process actually in the foreground is often go test or git rather than claude. A supervisor that looked for the name claude would call that pane dead, close the tab, and take the test run with it.

Scoping the check to one pane is what makes seven concurrent sessions supervisable at all. Each loop is looked up by label, then by tab, then by pane, and every pane is compared against its own shell. A box-wide pgrep claude returns seven pids and can’t say which of the seven is the dead one.

There’s a third state that isn’t obvious until you hit it. A session that reaches a usage limit doesn’t exit. It sits there holding the tick it was part-way through and picks it up again when the limit resets, which can be hours later.

That pane fails both of the tests above in the wrong direction. It has a live process, so the pid check calls it healthy, and it hasn’t printed anything for hours, so it looks stuck. Rebuild it and the half-finished tick is gone, and with a cron on ten minutes you destroy it again every ten minutes until the limit window closes.

Nothing exposes that state, so the only signal left is the text on screen:

LIMIT_PATTERN='hit your (session|weekly|usage|5-hour) limit|limit reached|limit exceeded'

Screen-scraping a CLI’s output is not a design I’m proud of, since it breaks the day Anthropic rewords that message, but it works and solves our problem for now.

We also added a setting called CLAUDE_CODE_RETRY_WATCHDOG that makes the CLI retry capacity errors indefinitely. A subscription usage limit has no setting, the reason we need to deal with it this way.


Ticks keep no state

Nothing on the box is worth backing up. Every tick reads what it needs from GitHub (we basically use it as a data source for orchestration): the open pull requests, the tracking issues where the sweeps record which areas they’ve claimed, the review comments. Recreating the VM costs exactly the ticks it missed.

The ten-minute cron is safe for the same reason. A restart is free, so the supervisor never has to weigh whether a rebuild is worth it. It also means the loops have no idea they were ever restarted, which removes the entire class of bugs where a resumed job double-processes its queue.


The intervals are the usage limit

As you probably know, a flat subscription doesn’t mean unlimited. It means a usage limit, and every interval in our loops is really an answer to the question of how much work fits inside one.

The cr loop costs more per tick than everything else combined, because it fans out one high-effort agent per pull request whose head it hasn’t reviewed. The sweeps are charged twice over: each PR a sweep opens is another PR cr then reviews. Those are the numbers we tuned. Ten minutes for cr, an hour for the sweeps and four for the test loops is what keeps seven loops running through a day without all of them parking on a limit at once.

There’s a second throttle in the skills themselves. A sweep with five or more of its own pull requests already open ends the tick instead of proposing a sixth. That ties the work to how fast we can review and merges rather than to the clock, and it stops the bot from burying us.


What came out

Pull requests opened by our bot, 7–18 August 2026
229 opened in 12 days
merged
213
landed on main
closed
5
closed without merging
open
11
in review at the time of writing

Separately, the dependabot loop drained 21 dependency bumps in the same window, merged strictly one at a time so that package lockfiles regeneration can’t race. Those PRs are authored by dependabot, so they aren’t in the count above.

The $240 also buys a calendar month, and we are twelve days into one. If the rate holds, the same bill covers something closer to 500 pull requests by the time it renews.

Again, these numbers measure volume, not quality, and I don’t want to claim otherwise. Every one of those PRs went through the same CI and the same review as a human’s, plus a high-effort review from cr on top, and nothing landed that wasn’t green or had an human behind the keyboard reviewing and approving it. That’s the bar we already had, and none of it moved, we just adopted our development process to the new agentic world we are living in.

I do believe the next steps would be defering to the bot more tasks around features and integrations with other systems to fix bugs and ship new features from other integrations, but we have not yet done that.


Wrapping up

I see every day a new tool solving this kind of issue with a new framework, a new platform and a new way that forces you to change the way you work. We opted to keep things simple and trusting some primitives that already exists for years, as we basically don’t need more than that.

My goal with this post was to show that you don’t need to go wild and reinvent the wheel every time you want to solve a problem, and neither spend a lot of time and money on it too. What we ended up with: a $200 subscription, a $40 VM, a multiplexer with a JSON CLI, /loop, and a supervisor script whose hardest job is telling three states apart. The state lives in GitHub, which was already our source of truth.

If you want to try it, my suggestions is to start with one loop, test, evaluate and iterate. The supervisor is only worth writing once you’ve had a tab sit there looking like it was working when it wasn’t.

Subscribe to RSS feed