two-cron-loops-run-my-13-product-portfolio

title: Two cron loops run my 13-product portfolio while I sleep date: 2026-08-20 description: The ops model behind a solo-operator SaaS portfolio: a 15-minute maintenance loop, a daily deep loop, and deploys gated hard enough that I sleep through them. tags: - claude-code - automation - ops - indie-hacker

TL;DR — My entire portfolio is maintained by exactly two scheduled Claude Code loops: a 15-minute maintenance chain and a daily deep chain. Everything they know lives in state files, everything they build happens in isolated git worktrees, and everything they deploy passes through one gated path that verifies the running binary's hash and rolls back on a failed health check. Last night, while I slept, they landed seventeen commits across eight products — including un-breaking a product whose deployed frontend was calling localhost. Here's the actual machinery.

I've written before about letting Claude Code run my monorepo from a Hetzner box. That post was the setup. This one is the operating model a few months in — what survived contact with reality.

Two loops, not ninety

I once had ~93 separately scheduled tasks. That's not automation, that's a second job. Today there are exactly two:

  • The frequent chain, every 15 minutes. Ten rotating maintenance steps: build/test a service, fill a coverage gap, dead-code sweep, dependency audit, security scan, API-contract check between frontend and backend, migration-drift check, landing-page audit, SEO pass, and a "lessons linter" that turns past incidents into grep checks. Each firing does one step and advances a cursor, so over days it sweeps the whole monorepo instead of hammering the same service.
  • The periodic chain, daily. Cheap reports first (build status, revenue pulse, uptime, failed-job triage), then the expensive work: research passes, cross-app audits, and a self-feeding product loop where one step mines real user pain points into a PRD and a later step implements the top PRD end to end.

Two entries under the runtime's task cap, zero scheduling sprawl, and when something misfires there are only two places to look.

State files are the contract

The non-obvious design decision that makes this work: loops keep durable state in files, not in memory. Every rotating step owns a small state file — a cursor, a checklist, a "here's what I did last and what's next" note. Each firing starts by reading its file and ends by writing it.

This matters because agent sessions are mortal. Context windows compact, sessions restart, the box reboots. If the loop's knowledge lives in the conversation, the loop dies with the conversation. If it lives in a file, any future firing — or any fresh session, or me at a keyboard — can pick up exactly where the last one stopped. The file is the contract between runs: plan on disk, progress on disk, decisions on disk. The agent is just the executor that shows up every 15 minutes and honors it.

A pleasant side effect: the state files double as an audit log. When a loop does something odd, I don't interrogate a chat history — I read its paper trail.

Build agents get their own worktrees

Multiple agents plus one shared git checkout equals mutual destruction. Early on, a feature agent running git checkout yanked HEAD out from under the maintenance loop mid-firing — three consecutive firings saw "reverted" files and stale cursors. A branch can only be checked out in one worktree at a time, so the fix was structural: every long-running agent works in its own git worktree, on its own branch. The maintenance loop has a dedicated worktree; ad-hoc build agents get disposable ones; server-side deploy builds happen in per-build worktrees rather than a shared checkout that anything might reset.

Isolation over coordination. Agents that can't see each other's working trees can't corrupt each other's working trees.

Deploys: gated, verified, reversible

Unattended deploys are the part that would keep me up at night, so they got the most paranoia. Everything funnels through one path:

  1. The service must be listed in a deploy manifest I've hand-verified. Not listed → refused, parked in the approval queue. The first unattended deploy of a service is never the thing that discovers its migration path.
  2. Database backup, then migrations.
  3. Snapshot the current binary, swap in the new one, restart.
  4. Smoke-check the health endpoint for a real, non-empty 200.
  5. On failure: automatic rollback to the snapshot, plus a circuit breaker — a halt flag that blocks all further deploys until a human clears it. One bad deploy stops the line; it doesn't start a cascade.

And because "the deploy script said success" is not the same as "the new code is serving traffic," verification checks what's actually running: read the live process's executable via /proc/<pid>/exe and compare its sha256 against the artifact that was just built. Same hash, or it didn't happen. This has caught real cases where a unit restarted but the binary swap hadn't landed — the class of failure where everything is green and nothing is true.

Hard limits

The loops run unattended, which is exactly why some things are structurally impossible rather than merely discouraged:

  • Approval queue for the irreversible. Money-moving Stripe calls, destructive SQL, anything outbound to real users, terraform, stopping a service — the loop does the safe part, writes the exact command to a queue file, and moves on. It never pings me; I clear the queue on my schedule. Pull, not push.
  • Embargoed hours and gated merges. Normal changes merge only on locally green builds and tests; risky windows and risky categories wait for a human.
  • Reports over interrupts. A daily briefing lands in Discord: what shipped, what failed, what's parked. I read it with coffee instead of getting paged at 3am.

One honest night

The night of August 19th, between 8:52pm and 1:18am, the loops landed seventeen commits across eight products: a cross-app audit that found and fixed billing bugs in two products (one was charging annual-plan pickers monthly — found by audit, not by an angry customer), a Stripe webhook-ordering guard, SEO plumbing and two blog posts, a fix for the logrotate bug that had been restarting five backends nightly, and — my favorite — the revival of a product that was quietly dead in production because its deployed frontend bundle had a hardcoded localhost API base. Every authenticated browser call had been failing for who knows how long. The audit caught it, the fix pointed it at same-origin, the deploy path shipped it, and I found out from the morning report.

None of that is glamorous work. That's the point. It's the maintenance that never wins prioritization against feature work, done continuously, by something that doesn't get bored.

The actual lesson

The leverage isn't "AI writes code." It's that the boring loop — audit, fix, test, deploy, verify, report — runs every 15 minutes whether or not I'm awake, and every escape hatch it could fall through has been welded shut in advance: state on disk, worktrees isolated, deploys manifested and hash-verified, disasters rolled back automatically, irreversible actions queued for a human. The autonomy was never the hard part. The guardrails were, and they're the reason I get to sleep through the interesting nights.