Four weeks ago I stood up an always-on Linux box that runs Claude Code agents while my laptop sleeps. It has logged over 800 agent runs since: six scheduled agents, five daily and one weekly, covering personal monitors, data pulls, and draft-for-my-approval writing tasks. This post is the step-by-step of how I built it, what it cost, and what I learned. (I'm keeping the specific workloads and the repo private; the recipe is the transferable part.)

The problem

Claude Code is an interactive tool. Close the laptop and the agent dies. I wanted agents that outlive my laptop: monitors that run every day whether I'm at a desk or not, and working sessions I can reach from my phone. The driver was cheap survivability, so every decision below is the boring, battle-tested, low-cost option.

Prerequisites

  • A Claude Max subscription. The flat rate suits "runs forever" better than metered API keys.
  • A cloud account. I used DigitalOcean because I already had tooling there. Any always-on Linux box works, including a Mac mini in a closet.
  • Tailscale, free for personal use.
  • A Telegram account for notifications.
  • 1Password (or any secrets manager with a CLI) so no secret ever lands on disk.

Step 1: One boring host

The host is a single DigitalOcean droplet: 1 vCPU, 2 GB RAM, about $12 a month. Shell scripts checked into a repo provision everything, so I can rebuild it from nothing. I tested that claim by rebuilding on a throwaway droplet, and the first attempt failed in three places the scripts silently assumed. One example: cron jobs ran with no HOME set, which worked on the hand-built box and broke on the fresh one.

Agents run as a dedicated admin user from cron. Root keeps only the health aggregator. I started with root-runs-everything and migrated after a permissions incident.

Step 2: Private by default

The box has no public door. The DigitalOcean firewall allows exactly one inbound rule: UDP 41641 for WireGuard. SSH, and later HTTPS for the phone app, ride the Tailscale tailnet. A health check probes the public IP from outside the tailnet and fails loud if anything answers.

One gotcha: doctl compute firewall create silently applies only the last --inbound-rules flag when you pass several. Build firewalls one rule per call and verify with a live probe, because the failure is invisible in the command string.

Step 3: Headless auth with no secrets on disk

claude setup-token (run once on any machine with a browser) mints a subscription-scoped OAuth token good for about a year. The token lives in 1Password and gets injected per run as CLAUDE_CODE_OAUTH_TOKEN; nothing persists in ~/.claude. A run wrapper refuses to run if ANTHROPIC_API_KEY is set, because that would silently switch billing from the subscription to metered API. A health check watches token age and warns before the yearly rotation.

As of July 2026, Anthropic grants Max plans a separate credit pool for headless and Agent SDK usage, so the fleet no longer competes with my interactive laptop quota. I keep usage overflow off, so credit exhaustion halts the fleet and pages me instead of silently metering.

Step 4: Runs are cron re-invocations

Each agent is an entry in a fleet.json: a name, a project directory, a cron schedule, a timeout, and a prompt. A generator turns that file into a crontab. Every run is a fresh claude -p invocation that starts, does its job, reports, and exits.

I rejected the persistent-session model because one immortal loop is one silent point of failure, and it accumulates context until it drowns. A bad cron run costs one cycle, and the next run starts clean. State that must survive between runs lives in files in the project directory.

Step 5: A control plane I already carry

Every run reports one line to Telegram. Routine traffic goes to a muted topic; failures interrupt me. Telegram won over Slack and push services because it is token-authed (no OAuth flow on the box), free, and two-way.

One rule anchors the whole system: agents draft, I send. Any outward action (an email, a post) ends as a draft or a queued proposal I approve from my phone. I enforce that line in what the agents can execute, not in a prompt.

Step 6: Health that pages

A cron aggregator reads per-agent status files and posts a daily fleet summary to Telegram. Each run writes OK or FAIL with an exit code, and a FAIL transition pages loud. As I write this, four agents are green and two are red from this morning's runs. I trust the summary because it shows me the red.

Step 7: Sessions from the phone

Cron covers monitors. For interactive work I use Happy (MIT): a CLI that wraps claude at the session layer and syncs the end-to-end-encrypted conversation through a relay to a mobile app.

I self-host the relay on the same box, bound to the tailnet only, fronted by tailscale serve for valid TLS on iOS. I build Happy from a pinned source tag rather than npm, because a young project that sits between me and my agents deserves a read of what I'm running. A daemon on the box makes sessions phone-spawnable, and a scheduled run can escalate: spawn a session, push a question to my phone, and wait for my answer.

Four weeks in

The numbers: 815 logged runs, 6 scheduled agents, 23 days of uptime since the last reboot drill, $12 a month plus the Max subscription I already paid for. The drafting agents produce output I actually send most weeks, and the data pulls run without me.

What I'd tell you to do differently than I did: drill everything live. Every claim in this post survived because a throwaway-droplet rebuild, a reboot drill, an off-tailnet probe, or a phone-escalation drill forced it to. The gap between "the script should work" and "I watched it work from nothing" is where all my incidents lived.